A GitHub Actions cache is a keyed, immutable archive scoped to one repository, capped at 10 GB by default and removed after 7 days without a read. Almost everything people call a caching bug is one of those four words arriving without a log line to announce it.

The lookup order and the three limits, from docs.github.com and the actions/cache source, both read on 2026-09-20.
Caching in Actions is not a shared filesystem and not a mount. Each entry is an archive stored against an exact string, uploaded when a job finishes and restored when a later job asks for the same string. Nothing is shared implicitly, and one character of difference is a complete miss.
That design is why caching is the largest speed win most pipelines have available and also the quietest way to lose minutes. A miss is not an error. The step prints a line, exits zero, and hands you the install you were trying to avoid.
What the action does with your key
The step declares one key and, optionally, a list of restore-keys. The action asks for the exact key first. On a match it restores that archive, sets cache-hit to true, and the post-job save step does nothing at all, because the entry it would write already exists.
On a miss it walks the restore-keys in the order you wrote them, treating each as a prefix and taking the most recent entry that starts with it. That is a partial restore: the files arrive, cache-hit stays false, and the save step at the end of the job writes a fresh entry under your exact key. If nothing matches either, the action prints Cache not found for input keys: followed by every key it tried, and the job continues.
- uses: actions/cache@v6
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
# then always reconcile, because a partial restore is not the lockfile
- run: npm ci
The
hashFilescall is the part that makes the key change when your dependencies change. A key built from a branch name or a static string produces a cache that never updates, which is worse than none: you install on top of stale content forever.
The three limits that decide whether anything is there to match
None of these three announce themselves. The entry is simply not there the next time you ask, and the only visible consequence is the install you thought you had eliminated.
| Limit | What GitHub does | What it looks like |
|---|---|---|
| Total size | 10 GB per repository by default, raisable by enterprise, organization or repository admins | Least recently used entries are evicted to make room. One large layer cache can clear out every dependency cache you had |
| Age | Entries not accessed in over 7 days are removed | A repository that is quiet at weekends is cold again on Monday, with no change to the workflow |
| Scope | A run restores caches from its own branch and the default branch; a pull request also reads its base branch | A feature branch cannot read a sibling branch cache, so every new branch starts cold unless the default branch warms it |
Quoted from docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching, read on 2026-09-20. Cache storage is capped rather than billed; artifact storage is billed against your plan.
Entries are immutable, and that changes how you design keys
GitHub is explicit about it: "You cannot change the contents of an existing cache. Instead, you can create a new cache with a new key." A second save under a key that already exists does not replace anything. If the content your job produces has changed but the key has not, the new content is simply never stored, and the next run restores the old content forever.
The design rule that falls out of this is short. Every input that changes what ends up in the cached directory belongs in the key. When you need to throw an entry away, you do not delete it, you roll a version prefix and let the old one age out.
# every input that changes the contents is in the key, and v2 is the
# handle you turn when you need a clean entry
key: v2-build-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
v2-build-${{ runner.os }}-
Cache or artifact
The two look similar and behave nothing alike, and using one for the other job is expensive in opposite directions. A cache is an optimization: keyed, evictable, free, and safe to lose. An artifact is an output: named, retained for a set number of days, billed against your plan storage, and safe to depend on.
| Cache | Artifact | |
|---|---|---|
| Addressed by | A key you compute | A name you choose |
| Survives | Until eviction: 10 GB cap or 7 days unread | The retention period you configure |
| Costs | Nothing, but capped | Billed against plan storage |
| Scoped to | Branch, with default-branch fallback | The workflow run |
| Use it for | Dependencies and build caches you can rebuild | Binaries, reports and anything a later job or a human needs |
Using an artifact as a cache works and costs money. Using a cache as durable storage works until the day it silently does not.
What the cache is worth in seconds
We ran the same install on a Latchkey latchkey-small runner with the npm cache emptied and then warm, on a lockfile with 414 packages: 4.2 seconds cold, 2.4 seconds warm, 2.3 seconds on a second warm pass. The restored directory was 20 MB. That is a small project, and the ratio matters more than the seconds: the gap scales with the size of the tree and with anything that compiles during install.
Worth knowing before you invest a day in cache keys: on the same runner, in the same script, restoring a Docker layer cache for a small image saved 13.7 seconds on a source-only change, and 12.3 seconds with nothing changed. If your pipeline builds a container, the layer cache is the bigger number by a wide margin.
Measured by
content/repro/timings/why-is-github-actions-slow/job-a.shon a Latchkeylatchkey-smallrunner on 2026-09-20, one pass per row, Node 20.20.2 and npm 10.8.2. The script and its output are committed beside each other.
Which version of the action to use
The current major is actions/cache@v6, and the README lists v6, v5, v4 and v3 as supported. The jump from v5 to v6 is not a runtime change, which is the thing people assume: both run on Node 24. v6 migrated the action to ESM and updated its dependencies. The runtime move happened at v5, which is also where the minimum Actions Runner version became 2.327.1, and that is the one worth checking if you host your own runners.
The save and restore actions published alongside the main one are the ones to reach for when you want to control when the write happens, for example warming a cache on the default branch in a job that does nothing else. Everything in this page about keys, prefixes, immutability and scope applies identically to all of them.
FAQ
What is the difference between artifacts and cache in GitHub Actions?
A cache is keyed for reuse by later runs, capped at 10 GB per repository, free and evictable. An artifact is named output, retained for a configured number of days and billed against your plan storage. Lose a cache and the next job is slower; lose an artifact and something that needed it is broken.
How much cache storage do GitHub Actions include?
10 GB per repository by default, shared across every entry for that repository, and enterprise owners, organization owners and repository administrators can raise it. It is a cap rather than a billed allowance. Cross it and the least recently used entries are removed silently to make room for the new one.
How long does a GitHub Actions cache last?
GitHub removes entries that have not been accessed in over 7 days, and an entry can go sooner if the repository crosses its size cap. Both happen with no line in any log, which is why a workflow that was fast last month can be slow today with nothing changed in the YAML.
Why does my cache miss on a pull request?
Caches are scoped by branch. A pull request run restores entries written by its own branch, by the default branch and by its base branch, and never 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 too.
Which version of actions/cache should I use?
Use v6, the current major as of September 2026. v5 is still maintained and runs on the Node 24 runtime, which needs Actions Runner 2.327.1 or newer on self-hosted runners. The key, restore-keys and path inputs behave the same across these versions, so upgrading is a version bump rather than a rewrite.
Top comments (0)