Skip to content
LatchkeyLatchkey home

GitHub Actions Caching: How the Cache Works, and Why It Stops Working

A GitHub Actions cache is a keyed archive scoped to one repository, capped at 10 GB in total, and evicted after 7 days without a read. Most "the cache stopped working" reports are one of those three facts arriving silently.

Caching in GitHub Actions is not a shared filesystem. Each entry is a tar archive stored against an exact key, uploaded when a job finishes and restored when a later job asks for the same key. Nothing is shared implicitly: if the key differs by one character, you get a miss and a full install.

That design is why caching is both the single largest speed win available in CI and the single most common source of quiet slowdowns. The cache never errors when it misses. It just costs you the minutes it was supposed to save.

How a cache entry is stored and matched

A cache step declares a key and, optionally, a list of restore-keys. On a hit for the exact key, the archive is restored and the save step at the end of the job is skipped. On a miss, GitHub walks restore-keys in order and restores the most recent entry whose key starts with one of those prefixes, then saves a fresh entry under the exact key when the job succeeds.

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

The three limits that break caches

Why a cache that used to hit starts missing

  • The lockfile changed, so the hash changed. Expected, and the reason restore-keys exists as a fallback.
  • Total size crossed 10 GB and the entry was evicted to make room for a larger one.
  • Seven days passed with no read, usually on a repository that is quiet at weekends or over a holiday.
  • The runner image or OS changed, so runner.os in the key resolves differently.
  • The job that would have saved the cache failed, and the save step only runs on success.

Docker layer caching is a separate problem

The Docker build cache does not live in actions/cache unless you put it there. On a fresh runner the daemon starts with no layers, so every FROM and every RUN re-executes. Buildx can export the layer cache to the Actions cache backend, which is what makes layer caching work across runs.

.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Ordering the cache steps so they actually help

  • Restore before install, save after. A save step placed before the install caches nothing useful.
  • Cache the package manager directory, not node_modules. Restoring a dependency tree built for a different platform is how you get errors that only reproduce in CI.
  • Give each language its own key and path rather than one combined cache, so one large ecosystem cannot evict the rest.
  • Warm the cache on the default branch on a schedule if your repository goes quiet for more than a week.

Measure before you optimise

Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.

Terminal
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'

# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40

Frequently asked questions

How much cache storage do GitHub Actions include?
Ten GB per repository on every plan, shared across all cache entries for that repository. It is a cap rather than a billed allowance: you are not charged for cache storage, but once the repository crosses 10 GB, the least recently used entries are evicted silently to make room.
How long does a GitHub Actions cache last?
An entry is removed after 7 days without being read, and can be evicted sooner if the repository hits the 10 GB cap. Both happen without a warning in the log, which is why a build that was fast last month can be slow today with no change to the workflow.
What is the difference between actions/cache and artifacts?
A cache is an optimisation keyed for reuse by later runs and capped at 10 GB per repository, free but evictable. An artifact is an output you intend to keep or download, retained for a configured number of days and billed against your plan storage allowance. Using an artifact as a cache is expensive; using a cache as durable storage loses data.
Why does my cache miss on a pull request?
Caches are scoped by branch. A pull request branch can read entries written by itself and by its base branch, but not entries written by a sibling branch. If the cache is only ever written on feature branches, every new branch starts cold. Write it on the default branch as well.
Does caching reduce my CI bill?
It reduces billed minutes, because a restored dependency tree removes the install time from every job that would otherwise repeat it. It does not reduce the per-minute rate. The two levers are independent: caching cuts the minutes, and the runner you choose sets what each minute costs.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card