Warm Cache

A warm cache is a cache that already holds the entries a run needs, so the run restores them and skips rebuilding them. What keeps a cache warm.

A warm cache is a cache that already holds the entries a run needs, so the run restores them and skips the work of building them again. A cache is warm for a given run only when the key that run computes finds a matching entry in a store the run is allowed to read.

Warmth is therefore a property of a lookup at a moment in time. The same store can be warm for one job and cold for the job beside it, because the two jobs compute different keys or read from different scopes.

Definition

A build cache is a keyed store. The workflow computes a key from its inputs, asks the store for an entry under that key, and either receives one or receives nothing. A hit means the run unpacks the entry into the paths it named and skips the step that would have produced those files. A miss means the run does the work and usually writes a new entry so the next run has something to find.

Warm names the state where that lookup hits. Cold names the state where it misses. Neither word describes the machine, the provider, or the workflow file. Both describe what the store contained at the moment the run asked.

That distinction matters because teams often describe a whole pipeline as warm or cold when the reality is per key. A job can restore its npm directory from a hit and then rebuild its Rust target directory from a miss in the same five minutes. Counting warmth per lookup is what makes a cache problem findable.

The four conditions behind a hit

A lookup succeeds when four separate conditions hold. Any one of them failing produces a miss, and the four fail for different reasons and leave different traces in the log.

ConditionWhat the store checksWhy it fails
KeyAn entry exists under the exact key this run computedAny input inside the key changed since the entry was written, such as a lockfile hash or an operating system prefix
CompatibilityThe entry was written for the same path set and with the same compression toolThe entry was saved on a different platform or with a different list of paths, so the store treats it as a separate entry
ScopeThe entry is visible to this run's branchThe entry was written on a sibling branch, which a run cannot read
RetentionThe entry is still in the storeThe entry aged past the retention window, or a size ceiling evicted it in least recently used order

GitHub documents all four for its own cache implementation: keys and restore-keys, the cache version derived from the compression tool and the cached paths, the rule that a run reads entries from its own branch and its base branch, the 7 day window after last use, and the 10 GB default ceiling per repository with least recently used eviction above it (GitHub dependency caching reference, checked on 2026-08-13).

Partial warmth

Most cache actions accept a list of fallback prefixes alongside the primary key. When the primary key misses, the store looks for the most recent entry whose key starts with one of those prefixes and restores that instead.

The result sits between warm and cold. A dependency directory restored from last week's entry still holds most of the packages the current lockfile asks for, so the install step runs and fetches the difference rather than the whole tree. The step still runs, and the work it does is smaller.

Two details follow from that and both catch people out. The hit output most actions expose stays false after a fallback restore, because it reports an exact primary key match only, so a step guarded on that output will still execute. And a save on top of a fallback restore carries forward whatever the older entry held, including files the current inputs no longer need, so entries drift upward in size across generations.

What keeps a cache warm

Three properties decide whether a cache is warm on the next run, and each maps to a decision someone made in the workflow file.

Key stability. A key derived from inputs that change on every commit guarantees a miss on every run. A commit SHA, a build number, and a timestamp all have this property. A key derived from a lockfile hash changes only when the dependency set changes, which is what makes the entry reusable across the commits in between. The rule of thumb is that a key should change exactly when the cached content should change.

Scope. Cache entries are partitioned so that one branch cannot read another branch's writes. A run reads entries written on its own branch and entries written on the branch it was created from. So the first run on a new branch is warm only if some earlier run on the base branch wrote an entry the fallback prefixes can reach. A workflow that writes entries on pull request branches only leaves every new branch cold.

Retention. Stores drop entries on two rules. Age since last use is the common one, and a size ceiling with least recently used eviction is the other. Both reward frequency. A key restored every weekday keeps resetting its own clock and stays warm indefinitely. A key touched once a month is cold every time it is asked for, whatever the workflow file says.

Example

Take a Node repository that caches its npm download directory. The workflow computes a primary key from the lockfile hash and lists one fallback prefix.

name: test
on: push

jobs:
  unit-tests:
    runs-on: warp-ubuntu-latest-x64-4x
    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 }}-npm-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-

      - run: npm ci
      - run: npm test

The workflow is new, so the store holds nothing for this repository yet. A developer creates the branch feature/checkout-copy from main and pushes to it three times.

Run 1, the first push on the new branch. The restore step computes Linux-npm-1a2b3c and finds nothing. The fallback prefix Linux-npm- also finds nothing, because no run has ever written an entry. The log reads Cache not found for input keys, and npm ci downloads every package in the tree. The post-run save step writes the entry under Linux-npm-1a2b3c.

Run 2, the second push on the same branch. The commit edits one source file and leaves package-lock.json alone, so the key is still Linux-npm-1a2b3c. The entry from run 1 is there, on the same branch, minutes old. The lookup hits, the log reads Cache restored from key: Linux-npm-1a2b3c, and npm ci installs from the restored directory with no downloads. The post-run save step skips writing, because an entry already exists under that exact key. That contrast is the whole idea: the same workflow file, the same machine size, and the difference between the two runs is what the store held when the run asked.

Run 3, a dependency added. package-lock.json changes, so the key becomes Linux-npm-4d5e6f and misses. The fallback prefix Linux-npm- matches the run 1 entry and restores it. The install fetches the new package and revalidates the rest. The save step writes a fresh entry under the new key, so the branch now has two entries.

The table below tracks the same store across five runs, including a sibling branch and the merge that seeds the default branch.

RunTriggerPrimary keyFallback prefixStateWritten after the run
1First push on feature/checkout-copymissmisscoldLinux-npm-1a2b3c
2Source-only push on the same branchhitnot usedwarmnothing, the key already exists
3Lockfile bump on the same branchmisshit on Linux-npm-1a2b3cpartly warmLinux-npm-4d5e6f
4First push on feature/error-copy, created from mainmissmisscoldLinux-npm-4d5e6f on that branch
5Push on main after the mergemissmisscoldLinux-npm-4d5e6f on main

Rows 4 and 5 are the scope rule in action. The sibling branch cannot read entries written on feature/checkout-copy, so it starts cold even though an identical entry exists a few commits away. Once run 5 writes the entry on main, every branch created from main can reach it through the fallback prefix, and the first push on a new branch lands warm. Seeding the default branch is the single change that moves most first-run misses.

Retention closes the loop. If the team leaves this repository alone for longer than the store's window, every entry ages out and run 6 is cold again on a workflow nobody edited. Cache misses that appear without a workflow change are usually retention or eviction rather than key drift.

One boundary is worth stating plainly. A restore repopulates the paths the workflow named and leaves the rest of the machine as it found it. Files outside those paths, such as a container image store or a compiler's incremental output directory, are gone when the machine is discarded, however warm the cache was. Carrying that state forward is a machine level mechanism rather than a cache lookup: the WarpBuild caching documentation covers the store side, and the snapshot runner documentation covers booting a job from a disk image captured during an earlier run.

FAQ

What is a warm cache?

A warm cache is a cache that already holds the entries a run needs, so the run restores them and skips the work that produced them. Warmth is measured per lookup: a cache is warm for a run when the key that run computes finds a matching entry in a store the run is allowed to read.

What makes a cache cold?

Four things, and any one of them is enough. The key changed, so no entry exists under it. The entry was written with a different path set or compression tool, so the store treats it as a different entry. The entry lives in a scope this run cannot read, such as a sibling branch. Or the entry aged out of the store or was evicted to make room.

How do you keep a cache warm across runs?

Keep the key stable, keep the scope shared, and keep the entry in use. Derive the key from inputs that change when the cached content changes, such as a lockfile hash, rather than from a commit SHA or a timestamp. Write the entry on the default branch so every branch created from it can fall back to that copy. And restore it often enough that it stays inside the retention window.

Is a warm cache the same as a warm machine?

No. A warm cache is a store the run reads from over the network into named paths. A warm machine is a runner whose disk already carries the state, including files no cache key covers, such as a populated container image store or a build tool's incremental output directory. A cache restore repopulates the paths you named and nothing else.

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.