How Long Do GitHub Actions Caches Last?
A WarpBuild cache entry expires 7 days after its last use, and every restore restarts that window. Key, cache version, and branch scope decide what survives.
Last verified:
A WarpBuild cache entry expires 7 days after its last use, and it can be deleted manually at any time from the action or the console. The clock counts from the last restore rather than from the moment the entry was written, so an entry that jobs keep asking for stays alive indefinitely, and one that nothing asks for drops out a week later.
Answer
The 7 day rule is documented in the WarpBuild caching documentation, and it is a sliding window. Every restore of an entry resets the countdown. A dependency cache on main that gets restored on every pull request run never reaches day 7, while the entry a feature branch wrote before the branch merged reaches it about a week after the last job touched that branch.
Expiry is only one of the four ways an entry stops being useful to you. The other three end its usefulness earlier, and they are the reason most teams believe their cache expired when it did not.
| What ends the entry | When it happens | What you see in the job log |
|---|---|---|
| Expiry after 7 days of no use | 7 days after the last restore or write | Cache not found for the exact key, no partial match either |
| Manual deletion | Whenever you run it, from the action or the console | Cache not found on the next run, immediately |
| A changed key | The next run after the key expression changes | Cache not found for the primary key, restore-keys may still match a stale entry |
| A changed cache version or branch scope | The next run on a different runner OS, a different path list, or a different branch | Cache not found even though the key string is identical |
The last row is the one that costs teams the most time. Cache version is a hash over the compression tool the runner uses and over the path list being cached, so two entries with the same key and different versions are two separate entries that never match each other.
WarpBuild caching is available on Linux and macOS runners. It is unsupported on Windows runners, per the limitations section of the caching documentation, so nothing in this page applies to a Windows job.
Detail
The window slides on every restore
Treat the 7 days as an idle timer on each entry rather than a retention period on the cache as a whole. Three consequences follow.
A hot entry never expires. A node_modules or Cargo registry cache keyed on a lock file that changes once a month is restored by every run in between, so each restore pushes expiry out another 7 days and the entry survives until the lock file moves.
A cold entry cleans itself up. When a feature branch merges and its branch-scoped entries stop being restored, they age out about a week later without anyone writing a cleanup workflow.
A weekend gap is safe and a holiday gap is not. A repository that goes quiet from Friday to Monday keeps everything. A repository that goes quiet for 10 days comes back to a full cold rebuild on the first run, because the entries crossed day 7 while nobody was pushing.
The Cache tab of the Billing report is where this is visible in aggregate. It shows total entries, storage cost, and operations cost for a date range, split by cache type across storage, operation-hit, and operation-commit. A storage number that falls over a quiet week and climbs again on the next busy one is the expiry window doing its job.
Three things make an entry a different entry
An entry is scoped to its key, its version, and its branch. Change any one of the three and the restore looks for something that was never written.
The key is the part you control directly. A key built from hashFiles on a lock file changes only when dependencies change, which is what you want. A key built from a commit SHA or a timestamp changes on every run, which writes a fresh entry every time and restores nothing. That failure mode is covered in why a GitHub Actions cache misses every run.
The version is a hash over the compression tool in use, which follows the runner OS, and over the path list. Adding one directory to path changes the version, so a workflow edit that looks harmless turns every existing entry into a miss on the next run.
The branch is the third axis. Entries are scoped to the branch that wrote them, with the default branch readable from branches cut off it. A feature branch restores what main wrote, and main does not restore what a feature branch wrote.
name: test
on:
push:
branches: [main]
pull_request:
jobs:
unit-tests:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: WarpBuilds/setup-node@v6
with:
node-version: 22
cache: npm
- name: Restore build cache
id: build-cache
uses: WarpBuilds/cache@v1
with:
path: |
.turbo
dist
key: ${{ runner.os }}-build-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-build-
- run: bun run build
- run: bun testTwo details in that file decide how long the entries last in practice. The restore-keys prefix means a run whose exact key misses still restores the newest matching entry, and that partial restore resets the 7 day window on the older entry, which keeps a fallback alive across dependency bumps. The path list of .turbo and dist is part of the version hash, so adding a third directory later starts a new entry family and leaves the old one to expire on its own.
A macOS entry cannot restore on a Linux runner
The version hash covers the compression tool, and the compression tool follows the runner OS. An entry written on warp-macos-15-arm64-6x therefore carries a different version from an entry written on warp-ubuntu-latest-x64-4x, and the two never match even when the key string and the path list are identical.
| Job that wrote the entry | Job that tries to restore it | Result |
|---|---|---|
warp-ubuntu-latest-x64-4x | warp-ubuntu-latest-x64-8x | Restores, since OS and paths match |
warp-ubuntu-latest-x64-4x | warp-ubuntu-latest-arm64-4x | Restores when the cached paths are architecture independent |
warp-macos-15-arm64-6x | warp-ubuntu-latest-x64-4x | Misses, since the cache version differs |
warp-ubuntu-latest-x64-4x inside a container without zstd | Any runner with zstd | Misses, since the action fell back to gzip |
The container row is the same rule wearing a disguise. When a job runs inside a custom container that lacks zstd, the action logs zstd version: null, falls back to gzip, and writes an entry under a different version. The next run on a normal runner asks for the zstd version and gets a 404. The fix is to make the compression tools match across every job that shares a key, and the caching documentation lists the wget, zstd, and token requirements for container jobs.
Plan for this when a matrix spans platforms. Give each platform its own key prefix, usually through ${{ runner.os }}, so a miss reads as a miss instead of looking like an expiry.
Deleting an entry before the 7 days run out
Waiting for expiry is the wrong move when an entry is wrong. A cache that captured a broken build output keeps handing that output to every job on the branch for up to a week, and each restore pushes the expiry out another 7 days, so a poisoned entry that stays busy never ages out at all.
The delete-cache input removes it in one run. Setting it to true skips both restore and save and deletes the entry for that key.
name: purge-build-cache
on:
workflow_dispatch:
jobs:
purge:
runs-on: warp-ubuntu-latest-x64-2x
steps:
- uses: actions/checkout@v4
- uses: WarpBuilds/cache@v1
with:
path: |
.turbo
dist
key: ${{ runner.os }}-build-${{ hashFiles('**/bun.lockb') }}
delete-cache: trueRun it on the branch that owns the entry, since entries are branch scoped. Entries can also be deleted from the WarpBuild console at any time, which is the faster route for a one-off. Wiring the workflow above to workflow_dispatch keeps the purge on hand without leaving a delete step in the normal build path.
What a live cache costs while it waits
Expiry has a bill attached, because storage is charged for as long as an entry lives. Rates come from the WarpBuild pricing page and the pricing table in the caching documentation, checked on 2026-08-13.
| Metric | Hosted runners | BYOC |
|---|---|---|
| Cache storage | $0.20 per GB-month | Free |
| Cache write, restore, or list | $0.0001 per operation | Free |
| Snapshot restore | $0.04 per job | Included |
| Snapshot storage | $0.025 per snapshot hour | Included |
Worked model on hosted runners. A repository keeps 25 GB of entries alive at any moment and runs 3,000 jobs a month, each doing one restore and one write.
- Storage: 25 x $0.20 = $5.00 per month.
- Operations: 3,000 x 2 x $0.0001 = $0.60 per month.
- Total: $5.60 per month.
Now add the branch churn that the 7 day window absorbs. Say 40 feature branches open in a month, each writing one 1.5 GB entry, and 12 of them are active in any rolling week. Only those 12 are billed, since the rest have crossed day 7 and been removed: 12 x 1.5 = 18 GB, or $3.60 per month, against the 60 GB and $12.00 that indefinite retention would carry. The window is the cleanup job you did not have to write.
Sizing the footprint deliberately is covered in the guide to the GitHub Actions cache size limit, and the per-gigabyte arithmetic in what GitHub Actions cache storage costs.
When the 7 day window is the wrong tool
Some state should outlive an idle week, and a keyed cache entry is a poor fit for it. Snapshot runners and remote Docker builders are the two parts of the product surface built for that, alongside CI observability, an MCP server, and the Action Debugger.
Snapshot runners capture a runner VM mid workflow and boot later jobs from that snapshot, which suits a long provisioning step that no key expression describes well. Remote Docker builders hold a persistent layer cache on the builder's own disk rather than in keyed entries, so repeat image builds reuse layers without a restore step in the job at all. Both are described in the guide to persistent caches for GitHub Actions.
Related Questions
What does holding a GitHub Actions cache for 7 days cost?
On WarpBuild hosted runners, cache storage is $0.20 per GB-month and each write, restore, or list operation is $0.0001. A repository holding 25 GB of live entries and running 3,000 jobs a month with one restore and one write per job pays $5.00 in storage and $0.60 in operations, or $5.60 a month. On BYOC, cache storage and operations are free. Full rates are on the WarpBuild pricing page.
Can two workflows in the same repository share one cache entry?
Yes. An entry is scoped to its key, its cache version, and its branch, and the workflow name is not part of that scope. Any job on the same branch that asks for the same key and the same path list restores the same entry, and that restore resets the 7 day window for both workflows. Keep the path lists identical across the two workflows, since the path list feeds the version hash.
How do I delete a WarpBuild cache entry before it expires?
Set delete-cache to true on the WarpBuilds/cache action, which skips restore and save and removes the entry for that key. Entries can also be deleted from the WarpBuild console at any time. Deletion is the tool for a poisoned entry, since waiting for the 7 day expiry leaves every job on that branch restoring the bad copy, and each of those restores pushes the expiry out another week.
Does restoring a cache entry extend how long it lasts?
Yes. The 7 day clock counts from last use rather than from creation, so an entry restored by a job on day 6 lives another 7 days from that restore. Entries that stop being restored, such as the ones left behind by a merged feature branch, age out on their own. Watch the totals on the Cache tab of the Billing report to see the steady state your repository settles into.
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.