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 entry | Ref recorded on the entry | Runs that can read it |
|---|---|---|
| Push to the default branch | refs/heads/main | Every run in the repository |
| Push to a feature branch | refs/heads/feature-a | Later runs on feature-a |
| Pull request run | refs/pull/318/merge | Later runs on that same pull request |
| Tag push | refs/tags/v1.2.3 | Later 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 testTrace 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.
| Order | Event | Ref of the run | Restore result | Entry written |
|---|---|---|---|---|
| 1 | Push to main | refs/heads/main | Miss, cache is empty | npm-Linux-9f2c1a on refs/heads/main |
| 2 | Push to feature-a, lock file unchanged | refs/heads/feature-a | Exact hit from the default branch | Nothing new to write |
| 3 | Push to feature-a after the bump | refs/heads/feature-a | Prefix hit on npm-Linux- from main | npm-Linux-4b77de on refs/heads/feature-a |
| 4 | Push to feature-b carrying the same bump | refs/heads/feature-b | Prefix hit on main again, the exact entry is out of scope | npm-Linux-4b77de on refs/heads/feature-b |
| 5 | feature-a merges, push to main | refs/heads/main | Prefix hit | npm-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 82397184Four 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.
Related Terms
- Cache key and how a restore request is matched: the key, the
restore-keysprefix list, and the version hash that decide the match once scope has allowed the lookup. - Do caches carry across branches in GitHub Actions: the direct answer for the branch case, with the fork and pull request variants.
- Sharing a build cache across repositories: the options once the repository boundary, rather than the branch boundary, is the constraint.
- GitHub caching documentation: the upstream reference for restrictions on accessing a cache.
- WarpBuild caching documentation: cache action inputs, key and version matching, and the entry expiry window.
- WarpBuild pricing: per minute rates by runner type.
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.