Skip to content

Blog

GitHub Actions cache limit: 10 GB, 7-day eviction, scoping and keys

·

GitHub gives every repository 10 GB of Actions cache, throws away anything not touched for seven days, and, when the repository is full, deletes the least recently used entries first. Those three rules explain most of the complaints about the cache: pull requests that never hit, a main cache that vanishes the morning after a busy afternoon, a job that restores 4 GB and takes longer than reinstalling.

This post is the whole set of rules in one place: the limits, what is evicted first, who can restore whose cache, the workflow pattern that works with those rules instead of against them, what is billed above the included size, and the access controls GitHub added in 2026. Every GitHub fact was read from GitHub’s own documentation or changelog on 10 November 2026, and the YAML is written for the current action majors on that date.

The limits, as of 10 November 2026

The default limit is 10 GB per repository, on every plan. GitHub removes any entry that has not been accessed in over seven days. A restore counts as an access, so an entry that is used regularly does not age out; one that is only ever written does. A key can be at most 512 characters, and an action given a longer one fails.

There are also rate limits per repository: 200 cache uploads, 1,500 downloads and 400 deletes a minute. Past them, uploads and downloads fail until the limit resets. You are most likely to meet them in a large matrix whose legs all save or restore at once.

What gets evicted first

When the repository goes over its limit, GitHub deletes entries in order of last access, oldest first. It does not look at how large an entry is, how expensive it was to build, or which branch saved it. GitHub itself warns that this “may cause cache thrashing, where caches are created and deleted at a high frequency.”

Put that beside the scoping rules in the next section and the usual failure follows. Every pull request that changes a lockfile saves a new entry that nothing else can read. Those entries count against the same 10 GB as the entry on main that every run actually needs. If main’s entry has not been touched recently, it is the oldest thing in the repository, and it is the one deleted. This is our inference from GitHub’s rules rather than something GitHub states, but it fits the symptoms, and the fix below is the same either way.

Who can restore whose cache

A workflow run can restore caches created on its own branch or on the default branch. It cannot restore caches created for child branches, sibling branches or different tag names. A run triggered for a pull request can also restore caches created in its base branch.

The detail that surprises people is how a pull request saves. A cache created by a run triggered on a pull request is created for the merge ref, refs/pull/N/merge. GitHub says it “cannot be restored by the base branch or other pull requests targeting that base branch” and “can only be restored by re-runs of the pull request.” So pull requests never warm each other. The only cache that helps a new pull request is one that the default branch saved.

Warm the default branch, restore on pull requests

The pattern that follows from those rules is to let one workflow own the writes. A workflow on main saves the cache, and pull request workflows only restore it. The saving workflow also runs on a schedule, because a restore counts as an access and a nightly run keeps the entry inside the seven-day window over a quiet week.

name: Warm the dependency cache on: push: branches: [main] schedule: - cron: "17 3 * * *" workflow_dispatch: jobs: warm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22 - uses: actions/cache@v6 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - run: npm ci

The pull request workflow uses the same path and key, but with actions/cache/restore, which never saves.

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

A pull request that does not touch the lockfile gets an exact hit. One that does touch it misses on the exact key and falls back to the newest entry that matches the restore-keys prefix, which is usually what main saved last. npm ci then downloads only the difference. Nothing the pull request does can crowd out the entry main depends on, because the pull request writes nothing.

The trade is that a pull request which adds a large dependency pays for it on every push to that pull request, since nobody saves the result. For most repositories that is a good trade. If it is not for yours, let the pull request workflow save as well, and accept the churn.

Seeing what is in your cache

Before changing anything, look. The repository’s Actions settings list every entry with its size, creation date and last-used date, and the GitHub CLI shows the same from a terminal. The largest entries first:

gh cache list --sort size_in_bytes --order desc --limit 20 # only what one pull request saved gh cache list --ref refs/pull/123/merge # machine-readable: createdAt, id, key, lastAccessedAt, ref, # sizeInBytes, version gh cache list --json key,ref,sizeInBytes,lastAccessedAt --limit 100

Three things are worth looking for. The same key prefix repeated hundreds of times means a part of the key changes on every run. Many entries on refs/pull/… refs means pull requests are saving. And a main entry whose last-used date is old means it is about to be, or has already been, evicted.

Cleaning up when a pull request closes

If pull requests do save, delete what they saved when they close. The entry is useless to everyone else and nothing else will remove it before it ages out. GitHub documents this pattern; the workflow needs actions: write and lists up to a hundred entries for the merge ref.

name: Clean up pull request caches on: pull_request: types: [closed] permissions: actions: write jobs: cleanup: runs-on: ubuntu-latest steps: - name: Delete this pull request's cache entries env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} PR_REF: refs/pull/${{ github.event.pull_request.number }}/merge run: | gh cache list --ref "$PR_REF" --limit 100 --json id --jq '.[].id' | while read -r id; do gh cache delete "$id" || true done

This is written against GitHub’s own cache service. For pull requests from forks, GitHub’s page suggests pull_request_target instead, and says it has security considerations of its own. Read those before using it. The rate limit on deletes is 400 a minute, which a single pull request will not approach.

Keys and restore-keys

A restore looks for an exact match on key, then for partial matches of key, then goes through restore-keys in order, looking for partial matches. If a restore key matches several entries, the most recently created one wins. If nothing matches on the current branch, the action tries again on the default branch.

On a miss, the action saves a new entry when the job succeeds. An existing entry cannot be changed: GitHub says to “create a new cache with a new key.” That one sentence is the whole of key design. The key must change exactly when the contents should change, and at no other time.

Our advice, which is opinion rather than a GitHub rule:

Build the key from the lockfile hash and the operating system, as above. Put nothing in it that changes on every run: a timestamp, the run number or github.sha make every run a miss and every run a save, which is the fastest way to fill 10 GB with entries nobody reads. Add a matrix dimension only when it changes what is cached. The cache version is stamped from the path and the compression tool, so entries for different paths are separate even under one key.

Order restore-keys from most to least specific, and stop at a prefix that still means something. A restore key that is only npm- matches every entry with that prefix, whatever its operating system or age, and the newest one wins. After a partial match the exact-key output is false, so run the install anyway; skipping it because something restored is how stale dependencies reach a build.

Cache the package store, not the install tree. ~/.npm holds compressed package tarballs; node_modules is a much larger tree of small files, slow to archive and slow to extract, and it goes stale whenever any dependency moves. Restoring a store and running the installer is usually the better trade.

Poisoning, the read-only default and cache-mode

Cache contents are not signed or verified, and any workflow run that can read a cache can extract its contents. So a cache that an untrusted run can write, and a trusted run later restores, is a way to carry code across a trust boundary. GitHub’s documentation says not to store secrets in a cache and to restrict cache writes to workflows triggered by trusted actors.

Since 26 June 2026, workflows triggered by events such as pull_request_target, issue_comment and workflow_run, where the run takes the default branch’s scope, get read-only access to that scope by default. A save from such a run logs a warning and the job carries on. Triggers that GitHub treats as trusted, such as push, schedule and workflow_dispatch, stay writable, and pull_request is unaffected. The remedy GitHub gives for a workflow that now cannot save is the one above: a separate workflow on a trusted trigger does the saving.

On 10 September 2026 GitHub made cache-mode generally available on all plans. It is a key, set at the top of a workflow or on a single job, that decides what the job’s cache token may do. The values are read (restore only), write (restore and save), write-only (save only) and none. A job-level value overrides the workflow-level one, the cache service enforces it, and it carries into reusable workflows: a called workflow cannot receive more access than its caller granted.

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

With cache-mode: read the ordinary actions/cache step restores and its save is skipped, so the same file serves a trusted push and a pull request without the split above. A blocked restore counts as a miss, and a blocked save is skipped with a message rather than an error. actions/cache 6.1.0, released on 26 June 2026, added handling for read-only access, so use that or later.

Two cautions. Declaring write or write-only on a low-trust event overrides the read-only default, and GitHub says that can increase the risk of cache poisoning; it adds a warning annotation when you do. And the version of actionlint we tried (1.7.12) rejects cache-mode as an unexpected key, so expect linters older than the feature to complain. This is GitHub’s feature, described in GitHub’s documentation; the version and syntax above are as GitHub documents them on 10 November 2026.

When 10 GB is not enough

Cache less first. Check what is in the list above, fix keys that vary, drop caches that save less time than they cost to restore, and move from install trees to package stores. Most repositories that think they need more space need fewer, better entries.

If it really is more than 10 GB, GitHub has let you raise the limit since 20 November 2025. Enterprise owners, organisation owners and repository administrators can set a cache size limit and a retention period in the Actions settings, and storage above 10 GB is billed. GitHub’s billing page lists Actions cache storage at $0.07 per GB per month (read 10 November 2026), measured as the peak for each hour, with the first 10 GB per repository included. Cache storage is a separate allowance from artifacts and Packages.

A budget on the cache storage line changes behaviour in a way worth knowing about before you set one. When it is reached, the cache becomes read-only until billing is resolved or usage falls back under the free 10 GB. A budget set below what your cache really costs can leave it read-only for much of the month. GitHub’s changelog says going beyond 10 GB requires a Pro, Team or Enterprise account; its caching page says user-owned repositories can be configured up to 10 TB and that the feature needs a payment method on file. Treat both as “a paid plan with a payment method” and check the settings page for your own account before planning around it.

The other option is a runner whose cache is larger. On our runners actions/cache works without a change to the workflow, the Actions cache is included rather than metered, and our published limits are 25 GiB per repository and 250 GiB per organisation, with the same rule that an entry not read for seven days is removed. One difference from GitHub is documented: a pull request job reads its own ref and the default branch, not its base branch, so the warm-default-branch pattern above works unchanged. The Actions cache reference has the details, including how deletes work there, and How the Actions cache works on our runners explains the mechanism.

A checklist

Look at gh cache list before you change a key. Have one workflow on the default branch own the saves, on a push and on a nightly schedule. Restore only on pull requests, or set cache-mode: read there. Build keys from the lockfile hash and the operating system, with nothing that changes per run. Cache the package store, not the install tree. Delete a pull request’s entries when it closes if it saves any. And decide deliberately whether you are paying for more than 10 GB, with a budget you have checked against what the cache really holds.

Docker layers can fill a cache quickly, and they have their own tools: see the post on Docker build cache and the Docker builds page. A disk that persists between jobs is a different mechanism with different rules, described in using sticky disks. Rates for what we meter are on the pricing page.

Run your CI and your infrastructure on one bill

Connect GitHub

Card required. You pay only for the minutes your jobs run.