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.

SegmentExample valueWhat a change in this segment means
PlatformLinuxThe entry was produced on a different operating system, and the binaries inside it may not run.
Ecosystem and versionnode-22-npmThe toolchain that produced the tree changed, so the layout or the compiled artifacts may differ.
Content hash3f9c1a... from a lockfileThe 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 repositoryKeyVersionResult on the next run
A dependency added to the lockfilechangesunchangedPrimary key misses, a fallback prefix may still restore the previous tree.
node-version bumped from 20 to 22changesunchangedPrimary key misses, and a prefix that includes the version misses with it.
The job moves from a Linux runner to a macOS runnerchangeschangesNo entry matches, and the job starts cold.
A second directory added to pathunchangedchangesNo entry matches even though the key text is identical.
The same workflow runs on a feature branchunchangedunchangedWhether 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 test

On 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:

RunLockfilePrimary keyWhat the restore step does
1initialLinux-node-22-3f9c1a4bNothing matches. npm ci downloads every package, and the job saves a new entry at the end.
2unchangedLinux-node-22-3f9c1a4bExact match. cache-hit is true and npm ci installs from the restored download cache.
3one package addedLinux-node-22-8e02d7c5The 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.

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.