Cache Key
A cache key is the string that names one cache entry. How GitHub Actions builds a key from the runner platform and a lockfile hash, and how a restore matches.
A cache key is the string that identifies one entry in a build cache, so a later job can ask for that exact entry by name. In GitHub Actions the key is usually assembled from the runner platform, the tool version, and a hash of a lockfile, which makes the string change at the moment the cached contents stop being valid.
Almost everything a cache does well or badly follows from that one string. A key that never changes serves the same stale tree forever, and a key that changes on every run writes a fresh entry every time and restores none of them.
Definition
A cache key is a string chosen by whoever writes the cache. The store treats it as opaque: it holds bytes under the key and returns those bytes when a later request presents the same string. Nothing inside the key is parsed by the store, so any structure it carries is a convention between the workflow author and the reader.
The convention is to join segments with a hyphen, ordered from the most general segment on the left to the most specific on the right. That ordering exists because fallback matching reads from the left, so a truncated key stays meaningful as a prefix.
| Segment | Example value | What a change in this segment means |
|---|---|---|
| Platform | Linux | The entry was produced on a different operating system, and the binaries inside it may not run. |
| Ecosystem and version | node-22-npm | The toolchain that produced the tree changed, so the layout or the compiled artifacts may differ. |
| Content hash | 3f9c1a... from a lockfile | The dependency set itself changed, so the stored tree is missing packages or holds packages nobody wants. |
In GitHub Actions the key is the required key input of the cache action. It accepts any combination of literals, context values, expressions, and functions, and the maximum length is 512 characters, with longer keys failing the step (GitHub dependency caching reference, checked on 2026-08-13).
What the key alone does not decide
Four properties surprise people who assume the key is the whole identity of an entry.
The key is evaluated when the step runs. hashFiles reads the files present on disk at that moment and returns a SHA-256 over the matched set, or an empty string when the pattern matches nothing (GitHub expressions reference, checked on 2026-08-13). A cache step placed before checkout therefore produces a key ending in a bare hyphen, shared by every run of the workflow.
The key is half of the identity. The cache action also computes a version from the list of path values and the compression tool available on the machine, and a restore matches on key and version together. The Cache Version section of the actions/cache README documents this behavior. Two jobs with identical key text and different path lists address different entries.
Entries are immutable. When an entry already exists for a key and version, the save step writes nothing and reports that the entry exists. Replacing cached content means computing a new key rather than overwriting the old one.
The cache-hit output is true only on an exact match against the primary key. A restore that lands through a fallback prefix sets it to false, which is why an install step guarded by if: steps.cache.outputs.cache-hit != 'true' still runs after a partial restore.
The table below separates the repository changes that move the key from the ones that move the version or the scope.
| Change in the repository | Key | Version | Result on the next run |
|---|---|---|---|
| A dependency added to the lockfile | changes | unchanged | Primary key misses, a fallback prefix may still restore the previous tree. |
node-version bumped from 20 to 22 | changes | unchanged | Primary key misses, and a prefix that includes the version misses with it. |
| The job moves from a Linux runner to a macOS runner | changes | changes | No entry matches, and the job starts cold. |
A second directory added to path | unchanged | changes | No entry matches even though the key text is identical. |
| The same workflow runs on a feature branch | unchanged | unchanged | Whether the entry is visible depends on cache scope. |
Example
This workflow caches the npm download cache with the key shape most Node repositories use: platform, tool version, then a hash of the lockfile, with two shorter prefixes as fallbacks.
name: test
on:
push:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore npm cache
id: npm-cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-22-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-22-
${{ runner.os }}-node-
- run: npm ci
- run: npm testOn a Linux runner runner.os renders Linux and hashFiles renders a 64 character hex digest, so the primary key resolves to a string such as Linux-node-22-3f9c1a4b.... The two restore-keys lines are prefixes of that same string with the most specific segment removed, so each one widens the search by one step.
Trace three runs of that workflow against an empty store:
| Run | Lockfile | Primary key | What the restore step does |
|---|---|---|---|
| 1 | initial | Linux-node-22-3f9c1a4b | Nothing matches. npm ci downloads every package, and the job saves a new entry at the end. |
| 2 | unchanged | Linux-node-22-3f9c1a4b | Exact match. cache-hit is true and npm ci installs from the restored download cache. |
| 3 | one package added | Linux-node-22-8e02d7c5 | The primary key misses. The prefix Linux-node-22- matches the run 1 entry, cache-hit is false, npm ci fetches only the new package, and a new entry is saved under the new key. |
Run 3 is the case the key shape is built for. The hash changed because the dependency set changed, so the exact entry is gone, and the prefix still finds a tree that is almost right. The choice of which segments to include and how many prefixes to list is covered on the sibling pages below.
Related Terms
- Cache scope and which runs may read an entry: the boundary that decides whether a branch can see an entry another branch wrote, which applies after the key matches.
- How to write a good cache key in GitHub Actions: a key template with each segment explained, and the two failure shapes a bad key produces.
- What restore-keys are in GitHub Actions: how prefix fallbacks resolve when several entries match, and when a partial restore hurts.
- GitHub dependency caching reference: the documented
keyinput, its 512 character limit, and the order a restore searches in. - WarpBuild caching documentation: cache action inputs, key examples, and the cache version rule.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What makes a good cache key?
A key that changes exactly when the cached contents should change. The common shape puts the runner platform first, then the ecosystem and its version, then a hash of the lockfile that determines what gets installed. Shorter prefixes of the same string go in restore-keys as fallbacks.
Does changing the cache key delete the old entry?
No. The previous entry stays in storage under its own key until it ages out or is evicted to make room. A new key writes a new entry, so a lockfile that changes every day leaves a trail of superseded entries behind it.
Can two jobs share one cache key?
Yes, when both jobs resolve the same key string, request the same path list, and sit inside a scope that allows the read. Two jobs with matching key text but different path values compute a different cache version and address different entries.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.