GitHub Actions cache size limit: why your cache is evicted, slow to restore, or missed
GitHub Actions gives each repository 10 GB of cache. Once the repository passes it, GitHub still saves the new entry and deletes the entries read longest ago until it is back under the limit, and it deletes any entry no job has read for 7 days. A branch reads only its own caches and the default branch's, so a new branch's first run often misses. And a hit is still a download: every job on a GitHub-hosted runner starts on a new VM, so it downloads and unpacks the archive again. You can raise the limit, at $0.07 per GB a month past 10 GB, and tune your keys, but the download stays.
Skipbox removes the download. Your jobs run on your account's own machine, which is kept between jobs, so the npm, pnpm, Gradle, Go and Cargo caches and Docker's layers are still on its disk from the last job (80 GB on S, against 14 GB on GitHub's runner). What actions/cache saves stays next to the machine, so a restore is a local read. Change one line, runs-on: skipbox, for a flat $39 a month with unlimited minutes.
GitHub's cache rules
From GitHub's dependency caching reference, Actions billing and its changelog on caches past 10 GB:
| Rule | What GitHub does |
|---|---|
| Size | 10 GB per repository by default, shared by every branch and workflow |
| Past the size | Saves the new entry, then deletes entries by last access, oldest first, until the repository is under the limit. Eviction runs every hour |
| Unused entries | Deletes any entry not read for over 7 days |
| A bigger limit | On a Pro, Team or Enterprise account, admins can raise the size and the days. Past 10 GB, $0.07 per GB a month, by each hour's peak. A budget reached makes the cache read-only until the next billing cycle |
| Branches | A run restores caches from its branch and the default branch; a pull request's run also from its base branch. Never a sibling or child branch |
| Pull requests | A pull request's caches are scoped to its merge ref: only reruns of that pull request read them |
| Entries | Can't be changed. A key that already exists isn't saved again |
| Hits | cache-hit is true only for an exact match of key; a restore-keys match is false |
| Rate | Up to 200 uploads and 1,500 downloads a minute per repository |
Why the cache is evicted
One 10 GB holds everything the repository saves. Each branch and each pull request saves its own entries. A matrix of three Node versions on two operating systems saves six. Docker's type=gha build cache lands in the same 10 GB. When a day's saves add up to more than the limit, each new save deletes an older entry, often the one the next job needs. GitHub calls this cache thrashing.
To see what fills it, list the entries by size:
gh cache list --sort size_in_bytes --limit 20
The repository's Actions › Caches page shows the same, with each entry's branch and last use.
Why the restore is slow
A cache hit on a GitHub-hosted runner is a download onto a new VM, then an unpack. Its time grows with the archive, so a cache that grew to several GB can take as long as the install it was meant to skip. A stalled download is cut off by actions/cache's segment timeout, 10 minutes by default (SEGMENT_DOWNLOAD_TIMEOUT_MINS).
Most setups also restore less than people think. setup-node's cache: input saves the package manager's download cache, not node_modules: after the restore, npm ci still installs every package. setup-python's pip cache is the same: the downloads come back, the environment is installed again.
Why the cache misses
- The key changed. A key with
hashFiles('**/package-lock.json')misses on every lockfile change.restore-keysgives a partial match, andcache-hitthen readsfalse. - A new branch. It can read the default branch's caches, not another feature branch's. If the default branch never ran the workflow, there is nothing to read.
- Evicted. The limit or the 7 days took it.
- It never saved. Entries can't be changed, so a key that exists is never saved again, and the cached content stays as old as its first save. A pull request from a fork may not be allowed to save at all.
Passing files to the next job of the same run is a different tool. The cache is for the next run; build output that a later job of the same workflow needs goes through actions/upload-artifact and actions/download-artifact. Artifacts have their own storage, apart from the 10 GB, and they are a download onto a new VM too.
What each fix costs on GitHub's runners
- Raise the limit. Fewer evictions, at $0.07 per GB a month past 10 GB (GitHub's own example: 200 GB fully used is $13.30 a month), on a Pro, Team or Enterprise account. Restores stay downloads, and bigger caches take longer to download.
- Save less. Cache
~/.npmor the pnpm store, notnode_modulesor build output. Save only on the default branch, withactions/cache/restorein every job andactions/cache/savein one. It costs your time, and a smaller cache restores less. - Fix the keys. Key on the lockfile, add
restore-keys, and run the workflow on pushes to the default branch so new branches have something to read. - A self-hosted runner that keeps its disk. No download, no 10 GB. You run the machine: OS updates, cleaning between jobs without deleting the caches, and fixing it when it breaks.
When the fix is a machine that keeps its disk
The cache exists because the machine forgets. A GitHub-hosted runner is a new VM for every job, so the job saves its state to GitHub's storage and fetches it back next time. On a machine that stays, that state is already on its disk: the package manager's cache, the tool cache, Docker's images and build cache, even the build output.
The difference shows in real projects' CI, from our runs of their own jobs:
- astro (Node.js, Turborepo): 528 s on GitHub, 30 s on a Skipbox own machine, which keeps turbo's cache, so its warm runs replay 40 build tasks instead of running them.
- helix (Rust): the toolchain-and-cache step took 9-10 s on the own machine, against 41-46 s on GitHub.
- tauri (Rust): 246 s on GitHub, 53 s on the own machine, with
target/kept between jobs.
Skipbox keeps the cache on your machine
Each plan comes with your account's own machine, kept between jobs:
- Package caches stay on the disk:
~/.npm, the pnpm store, each tool's~/.cache, and the Go, Gradle, Cargo and Maven caches, with the toolssetup-*installed. Nothing to restore. - Docker's images, layers and build cache stay too.
docker buildreuses the last build's layers (Docker layer cache). - **actions/cache, setup-* and rust-cache need no change.** What they save stays next to the machine instead of in GitHub's storage, with more room than GitHub's 10 GB per repository, and a restore reads it locally.
- No 7-day expiry on the disk. Nothing is deleted until the disk is nearly full after a job; then what was used longest ago goes first.
At least 7× GitHub's cache room on S. A job picks its size by label:
| Label | Machine | Disk | Plan |
|---|---|---|---|
skipbox | 4 vCPU · 16 GB | 80 GB | S, $39 a month |
skipbox-8x16 | 8 vCPU · 16 GB | 160 GB | M, $79 a month |
skipbox-16x32 | 16 vCPU · 32 GB | 320 GB | L, $149 a month |
A private repository's ubuntu-latest has 2 CPUs, 8 GB and 14 GB of SSD. Minutes are unlimited on Skipbox. At 20,000 minutes a month on 4 vCPU, GitHub bills $222; Skipbox S is $39, $183 less.
Moving is one line
jobs:
test:
runs-on: skipbox # was ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci && npm test
- Sign in to Skipbox with GitHub.
- Install the Skipbox GitHub App and pick the repositories.
- Change
runs-onand push.
Keep your cache steps as they are. The second run finds the first run's caches on the machine.
Security
- The machine is your account's alone. No other company's jobs run on it.
- After every job, the runner's credentials, processes and containers are wiped. Clean every job gives each job a fresh machine instead.
- Code from people without write access (such as pull requests from forks) runs on a fresh VM that reads your caches but can't change them.
The details are in how jobs are isolated.
Try it on the job whose cache misses
The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move that job to runs-on: skipbox, run it twice, and put the second run's restore and install steps next to GitHub's.
FAQ
What is the GitHub Actions cache size limit?
10 GB per repository by default, for every branch and workflow together. Past it, GitHub still saves the new entry and deletes entries in order of last access, oldest first, until the repository is under the limit again. Entries no job has read for 7 days are deleted whatever the size. On a Pro, Team or Enterprise account, admins can raise the limit and the days; storage past 10 GB is billed at $0.07 per GB a month, measured by each hour's peak, and once a budget is reached the cache turns read-only until the next billing cycle.
Why is my GitHub Actions cache evicted so often?
Because everything in the repository shares one 10 GB: each branch, each pull request, each matrix job and each OS saves its own entries, and Docker's type=gha cache lands in the same place. When a day's saves add up to more than the limit, every save deletes an older entry, and the next job misses the one it needed. GitHub calls this cache thrashing. List the entries by size (gh cache list --sort size_in_bytes) to see what fills it.
Why does restoring the cache take as long as installing from scratch?
Because a hit is still a download. Each job on a GitHub-hosted runner starts on a new VM, so actions/cache downloads the archive and unpacks it every time, and the bigger the cache the longer that takes. setup-node's cache brings back the package manager's download cache, not node_modules, so npm ci still installs every package after the restore. On a runner that keeps its disk, the caches are already there and nothing is downloaded.
Why does actions/cache miss on a new branch?
A run can restore caches from its own branch and from the default branch, and a pull request's run also from its base branch. It can't read a sibling branch's caches. So a feature branch's first run hits only if the default branch saved that key. Run the workflow on pushes to the default branch so it saves, and add restore-keys for a partial match; cache-hit is true only for an exact match of key.
How much Actions cache does Skipbox keep?
More than GitHub's 10 GB per repository, next to your machine instead of in GitHub's storage. On a cloud machine, what jobs save with actions/cache, setup-* and rust-cache stays on the machine's own disk, up to half of that disk (80 GB on S, 160 GB on M, 320 GB on L); on a Skipbox host, the host's cache keeps 20 GB per repository. Either way an entry is kept 7 days after it was last used, with GitHub's branch rules. Package caches and Docker's layers don't go through it: they stay on the disk itself.
What clears the caches on a Skipbox machine?
A reset: when the machine fails its health check after a job, when you press Reset machine, when its image is updated (at least once a month), and when a job reaches the 6-hour limit. The machine is deleted with its disk when the plan ends. Between those, nothing is deleted until the disk is nearly full after a job; then what was used longest ago goes first.
How much does it cost to try?
Nothing. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move the job whose cache misses or restores slowly to runs-on: skipbox, run it twice, and compare the second run's restore and install steps with GitHub's.
Updated November 13, 2026