DevOps · Cicd

GitHub Actions Caching — A Field Guide

GitHub Actions Caching — A Field Guide is the work that turns a deploy into a system. The deployment is one moment; the system is the next 18 months of uptime, incidents, and.

John Kihiu12 min read

The single biggest lever on GitHub Actions build time is usually not a faster runner — it's not redoing work the previous run already did. actions/cache is the mechanism, and the difference between a cache that actually hits and one that silently misses every run comes down to getting the key right.

How actions/cache actually works

The action takes a path (or list of paths) to save and a key to save it under. On a cache hit — an exact key match — the paths are restored before your steps run. On a miss, the job runs normally and, if you're using actions/cache directly (not cache: npm style shortcuts), a new cache is saved under that key at the end of the job, but only if the job succeeds. Caches are scoped per repository and, for pull requests, fall back to caches from the base branch — a PR can read caches from main but a push to main can't read a PR's cache.

Getting the cache key right

The key should be built from something that changes exactly when the cached content should change — almost always a hash of the lockfile. Hashing package-lock.json, yarn.lock, or Cargo.lock means the cache invalidates precisely when dependencies change, not on every commit and not never. restore-keys gives you a fallback: if the exact key misses, it restores the most recent cache matching a key prefix, which is close enough to warm up incremental tools like a compiler cache even when the lockfile changed.

YAML · NODE DEPENDENCY CACHE
- uses: actions/checkout@v4

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

- run: npm ci
- run: npm test
Use the built-in shortcut when your tool supports it

actions/setup-node, actions/setup-python, and similar setup actions have a cache: input (e.g. cache: 'npm') that wraps this exact pattern for you — correct key hashing included. Reach for actions/cache directly only when caching something the setup action doesn't cover, like a build output directory or a compiler cache.

What belongs in the cache, and what doesn't

Package manager download caches (npm, pip, Maven's .m2, Go's module cache) are the clearest win — they're pure downloaded content keyed cleanly off a lockfile hash. Build artifacts and compiler caches (Rust's target/, Docker layer caches via actions/cache plus buildx, ccache) also cache well but need care with the key, since they can grow large and stale entries just waste cache storage without speeding anything up. node_modules itself is a common one to avoid caching directly — reinstalling from a cached npm store via npm ci is usually faster and safer than restoring a potentially inconsistent node_modules tree.

Caches are not backups and not guaranteed

GitHub evicts caches after 7 days of no access and enforces a per-repository size cap (10 GB total), evicting the least recently used caches once you're over. A workflow must succeed correctly on a full cache miss — never assume the cache will be there.

Cross-job and cross-workflow scoping

Caches saved on a branch are visible to workflow runs on that branch and, for the default branch, to all branches as a fallback via restore-keys. This is why a cache warmed on main speeds up the first run on a new feature branch, but a cache built on a feature branch doesn't leak back into other unrelated branches. Matrix jobs each need their own key component (typically the matrix variable) or they'll fight over — and possibly corrupt — the same cache entry.

Cache candidateKey basisNotes
npm/yarn/pnpm storelockfile hashUse setup-node's built-in cache input
pip/poetryrequirements/poetry.lock hashUse setup-python's built-in cache input
Docker layersDockerfile + context hashPair with buildx cache-from/cache-to
Compiler cache (ccache, sccache)source hash or rolling keyWatch total size vs the 10 GB repo cap

Wrapping up

Cache keys built from a lockfile hash, with a sane restore-keys fallback, get you most of the win with none of the surprise. Prefer the setup actions' built-in cache: input where it exists, keep matrix jobs from sharing a key, and remember caches can and will be evicted — the workflow has to work correctly without one.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.