Speeding up GitHub Actions: A Practical Guide to Workflow Caching
Learn how to cut CI build times by 30–70% with GitHub Actions caching. This guide covers key concepts, a concrete example, trade‑offs, and actionable steps to implement and verify a cache‑enabled workflow.
24 Jul 2025, 10:14 UTC

The Problem: Repetitive Dependency Fetching Slows CI
Every time a workflow runs, it often downloads large dependency archives (e.g., node_modules, ~/.m2/repository, ~/.gradle/caches) and rebuilds artifacts. In monorepos or projects with heavy build steps, this can add 15–30 minutes to a run. CI teams report that the majority of the job’s time is spent on network I/O and compilation, not on the actual tests or deployment steps.
Thesis: Use GitHub Actions Cache to Cut Build Time by 30–70%
GitHub provides a built‑in actions/cache action that stores a tarball of selected files in a per‑repository cache. On subsequent runs, the action restores the files before the job’s steps, making the cache appear as if the files were already present on the runner. When configured correctly, the cache can reduce dependency download time and compile steps by a large margin.
Key Concepts
- Cache key: A unique string that identifies a cache entry. It is usually a hash of lockfiles or configuration files that change when dependencies change.
- Paths: The directories or files that the cache will store. These should be read‑only for the workflow (e.g.,
node_modules,~/.m2/repository). - Cache limits: 5 GB per repository, 10 GB per workflow, 7‑day expiration after no hit.
- Branch‑specific keys: Append
${{ github.ref_name }}to the key to keep caches separate per branch. - Cache hit/miss: The action logs
Cache hitorCache missand extracts or creates the cache accordingly.
Concrete Example: Node + Maven Project
Below is a minimal workflow that caches both Node dependencies and Maven artifacts. The cache key includes the lockfile hashes and the branch name to avoid stale caches across branches.
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ---- Cache Node dependencies ----
- name: Cache Node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ github.ref_name }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install JS deps
run: npm ci
# ---- Cache Maven artifacts ----
- name: Cache Maven repo
uses: actions/cache@v3
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}-${{ github.ref_name }}
restore-keys: |
${{ runner.os }}-maven-
- name: Build & Test
run: mvn -B test
Explanation of the key:
${{ runner.os }}ensures separate caches for Windows, macOS, and Linux runners.hashFiles('**/package-lock.json')generates a SHA‑256 hash of the lockfile. If the lockfile changes, the key changes and the cache is invalidated.${{ github.ref_name }}appends the branch name, preventing a branch‑specific cache from being overwritten by another branch.
Verification Checklist
- Run the workflow without caching and note the total duration (e.g., 25 min).
- Enable caching as shown above and run again. Observe a
Cache hitlog and a reduced duration (e.g., 12 min). - Change a dependency in
package-lock.jsonorpom.xml, commit, and run. The cache should miss, rebuild dependencies, and the duration should increase again. - Navigate to Actions > Settings > Actions cache to confirm the cached entry size and key.
- For deeper insight, set
ACTIONS_CACHE_DEBUG=truein the workflow environment to see detailed hit/miss diagnostics.
Trade‑offs and Limitations
- Size limits: A single cache entry is capped at 5 GB. In large monorepos, the combined size of
node_modulesand Maven repos can exceed this, triggering eviction of older entries. Monitor the cache size and consider splitting caches per service. - Stale data: If the cache key does not change when a lockfile is updated, you may use outdated dependencies. Always include the lockfile hash in the key.
- Security: Cached files are stored unencrypted. Never cache directories that contain secrets (e.g.,
~/.aws). - Failure propagation: A corrupted cache can be reused by subsequent runs. Add a post‑step that clears the cache on failure:
- name: Clean cache on failure if: failure() uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ github.ref_name }} restore-keys: | ${{ runner.os }}-node- # Set to 'delete' mode by passing an empty key key: '' - Policy changes: GitHub may adjust cache limits or retention policies. Verify the current limits in your organization’s settings before relying on caching for critical pipelines.
Actionable Steps for Your Team
- Audit Dependencies: Identify directories that are large and static between runs (e.g.,
node_modules, Maven repos). - Define Cache Keys: Use lockfile hashes and branch names. Avoid generic keys that can be shared across unrelated branches.
- Implement Caching: Add the
actions/cachesteps before the install/build steps. Test in a separate branch first. - Measure Impact: Compare run times with and without caching. Document the savings.
- Monitor Size & Health: Periodically check cache size in the Actions settings and add a cleanup step for corrupted caches.
- Document the caching strategy in your CI/CD guide so new contributors understand the key structure and limitations.
By following these steps, you can reliably reduce CI run times, free up runner resources, and keep your pipelines responsive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.