Cache Scope

Cache scope is the boundary that decides which GitHub Actions runs may read a cache entry: the branch that wrote it, plus the default branch.

Cache scope is the boundary that decides which workflow runs are allowed to read a cache entry. A run reads entries written on its own Git ref plus entries written on the repository's default branch, and entries written anywhere else are refused.

Scope is checked before the key. An entry whose key matches a request exactly still produces a cache miss when that entry sits outside the requesting run's scope.

Definition

Every GitHub Actions cache entry carries three pieces of identity: the repository it belongs to, the Git ref of the run that wrote it, and the key the run supplied. Scope is the repository and ref part of that identity. GitHub describes the rules under restrictions for accessing a cache in the caching documentation, checked on 2026-08-13.

Two boundaries apply, in order.

The repository boundary comes first and has no exceptions. An entry belongs to one repository, and a workflow in a different repository is refused whatever key it asks for. Teams that want one dependency cache across several repositories have to move the storage out of the per repository cache service, which the guide on sharing a build cache across repositories covers.

The ref boundary comes second and is where the surprises live. A restore request searches the run's own ref first, then the default branch, and stops at the first match. Nothing else is searched.

Event that wrote the entryRef recorded on the entryRuns that can read it
Push to the default branchrefs/heads/mainEvery run in the repository
Push to a feature branchrefs/heads/feature-aLater runs on feature-a
Pull request runrefs/pull/318/mergeLater runs on that same pull request
Tag pushrefs/tags/v1.2.3Later runs on that tag

The first row is asymmetric on purpose. Work done on the default branch flows outward to every branch, and work done on a branch flows nowhere until the change merges and a run on the default branch writes the entry again. A long lived branch that installs its own dependency set pays the install cost on every run of that branch, then pays it once more on the first run after the merge.

Pull request runs sit in a scope of their own because the ref they record is the merge ref rather than the head branch. Two runs on the same pull request share entries with each other. A push run on the same head branch records refs/heads/feature-a instead, so the two event types on one branch keep two separate sets of entries.

Scope decides eligibility only. Inside the right scope a request still has to match on the key and on the entry version, which is derived from the list of cached paths and the compression tool the writing runner used. The cache key page covers that half of the match.

Example

This workflow caches the npm download cache on every push and every pull request. It uses an exact key built from the lock file hash and a prefix fallback:

name: test
on:
  push:
  pull_request:

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-
      - run: npm ci
      - run: npm test

Trace five events through a repository whose default branch is main. Lock file hash 9f2c1a is the state on main, and 4b77de is the state after a dependency bump.

OrderEventRef of the runRestore resultEntry written
1Push to mainrefs/heads/mainMiss, cache is emptynpm-Linux-9f2c1a on refs/heads/main
2Push to feature-a, lock file unchangedrefs/heads/feature-aExact hit from the default branchNothing new to write
3Push to feature-a after the bumprefs/heads/feature-aPrefix hit on npm-Linux- from mainnpm-Linux-4b77de on refs/heads/feature-a
4Push to feature-b carrying the same bumprefs/heads/feature-bPrefix hit on main again, the exact entry is out of scopenpm-Linux-4b77de on refs/heads/feature-b
5feature-a merges, push to mainrefs/heads/mainPrefix hitnpm-Linux-4b77de on refs/heads/main

Step 4 is the whole idea. An entry with the exactly correct key already existed when feature-b ran, and feature-b still installed from a stale partial restore, because that entry belonged to feature-a. Step 5 fixes it for everyone: once the bump lands on the default branch, the next run there writes npm-Linux-4b77de in the scope every branch can read.

Reading the scope of live entries

The REST API returns the ref alongside each entry, so the scope of a repository's cache is directly observable:

gh api repos/:owner/:repo/actions/caches \
  --jq '.actions_caches[] | "\(.ref)\t\(.key)\t\(.size_in_bytes)"'
refs/heads/main         npm-Linux-9f2c1a        82134528
refs/heads/feature-a    npm-Linux-4b77de        82397184
refs/heads/feature-b    npm-Linux-4b77de        82397184
refs/pull/318/merge     npm-Linux-4b77de        82397184

Four rows, three of them holding a near identical payload under three refs. Duplicate keys across refs are normal output rather than a fault, and they are the usual reason a repository sits near its storage limit while individual jobs still report misses. gh cache list --ref refs/heads/feature-a narrows the same listing to one scope, which is the fastest way to confirm whether a branch is reading its own entry or borrowing the default branch entry.

FAQ

What decides the scope of a GitHub Actions cache entry?

The Git ref of the run that wrote it, inside the repository that owns it. A push run records refs/heads/<branch>, a pull request run records refs/pull/<number>/merge, and a tag run records refs/tags/<tag>. That ref is the boundary every later restore request is checked against.

Can one feature branch read a cache written by another feature branch?

No. Two feature branches are two separate scopes, so an entry written on feature-a is invisible to feature-b even when the keys match exactly. Both branches can read entries written on the default branch, which is why shared dependencies are usually seeded by a run on main.

Why does a cache stop being found once a pull request is opened?

A pull request run records the merge ref rather than the head branch, so entries written by push runs on that same branch sit outside its scope. The run still falls back to the default branch, and entries it writes are readable by later runs on the same pull request.

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.