GitHub Actions cache: keys, scopes and the limits that decide a hit
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.

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 ciThe 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 |
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 |
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 the same commit. If your pipeline builds a container, the layer cache is the bigger number by a wide margin.
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.
Frequently asked questions
What is the difference between artifacts and cache in GitHub Actions?
How much cache storage do GitHub Actions include?
How long does a GitHub Actions cache last?
Why does my cache miss on a pull request?
Which version of actions/cache should I 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.