Can I Share a Cache Between GitHub Actions Workflows?
Yes. Cache entries are addressed by key, version, and branch rather than by workflow, so any workflow in the same repository can restore what another saved.
Last verified:
Yes. A cache entry is addressed by three things, its key, its version, and the branch that wrote it, and the name of the workflow that created it is absent from that address, so any workflow in the same repository can restore an entry that another workflow saved. The work is in making the address match: two workflows that ask for the same key but cache a different list of paths compute a different version and miss each other silently.
Answer
The address of a cache entry has exactly three parts.
The key is the string you write in the key input. It is yours to choose, and it is the only part of the address you control directly.
The version is a hash generated over the compression tool in use and the list of paths being cached. It is computed by the caching client rather than written by you, and it is matched alongside the key. The hash function lives in the actions/toolkit cache package, and the WarpBuild caching documentation describes the same behavior for WarpBuilds/cache@v1.
The branch scopes visibility. An entry written on a branch is restorable from that branch, and entries written on the default branch are restorable from branches cut off it, per GitHub's dependency caching reference.
Everything else about the run is absent from the address. Here is the full split.
| Property of the run | Part of the cache address | What it means in practice |
|---|---|---|
key input | Yes, directly | Two workflows sharing an entry must write the same key or a matching restore-keys prefix |
List of cached path entries | Yes, through the version hash | Adding one directory to the list in one workflow breaks sharing with the other |
| Compression tool (zstd or gzip) | Yes, through the version hash | A container without zstd falls back to gzip and computes a different version |
| Branch that saved the entry | Yes | Feature branches read the default branch; sibling feature branches do not read each other |
| Workflow file name | No | build.yml and test.yml reach the same entry |
| Job name and job id | No | Renaming a job leaves the address unchanged |
| Runner size | No | An entry saved on an 8 vCPU runner restores on a 2 vCPU runner |
| Runner OS and architecture | Indirectly | Different platforms mean different compression tooling, so different versions |
So the answer to the question is yes, within one repository, subject to the branch rule. Cross-repository sharing is out of scope for the cache, and the share data between GitHub Actions jobs guide covers the handoff patterns that do cross those boundaries.
WarpBuild's cache follows the same addressing model. WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4, accepting the same key, path, and restore-keys inputs, so a workflow that already shares a cache correctly keeps sharing it after the swap. The cache is enabled by default on Linux runners and is not supported on WarpBuild Windows runners, which is documented under limitations in the caching documentation.
Detail
What actually blocks sharing
Nearly every failed sharing attempt is a version mismatch rather than a key mismatch, because the key is visible in the workflow file and the version is not.
Consider two workflows that both want the dependency cache. The first caches one directory:
- uses: WarpBuilds/cache@v1
with:
path: node_modules
key: deps-${{ hashFiles('bun.lock') }}The second caches two:
- uses: WarpBuilds/cache@v1
with:
path: |
node_modules
.cache/turbo
key: deps-${{ hashFiles('bun.lock') }}The keys are identical. The versions are not, because the path list feeds the hash. The second workflow will never restore what the first one saved, and neither run reports an error. The restore step logs a miss and the job carries on doing the work the cache was supposed to remove.
The same mechanism explains the platform rule. An entry saved on warp-macos-latest-arm64-6x cannot restore on warp-ubuntu-latest-x64-4x, because the compression tool differs between the two and the version therefore differs. A key containing ${{ runner.os }} makes that separation visible in the key as well, which is worth doing purely so the miss is legible in the log.
Containers are the third case. WarpBuild runners ship zstd, but a job running inside a custom container image that lacks it falls back to gzip. The restore step logs zstd version: null followed by a 404 warning, which is the version mismatch surfacing as a missing entry. Installing zstd in the container image restores the match.
The branch rule, and how to work with it
Branch scope is the second constraint, and it is the one that decides where the entry should be written.
An entry saved on a feature branch is not readable from another feature branch. An entry saved on the default branch is readable from branches based on it. That asymmetry has one clean consequence for cross-workflow sharing: the workflow that writes the shared entry should run on the default branch, and the workflows that consume it should restore only.
Splitting write and read is what WarpBuilds/cache/restore@v1 and WarpBuilds/cache/save@v1 exist for. The consuming workflows use the restore half and never write, so pull request runs cannot poison the shared entry, and storage does not grow one entry per branch.
A shared prefix and restore-keys across two workflows
This is the pattern the question is usually asking for. One workflow on the default branch owns the entry. Another workflow on pull requests reads it. The path block and the key prefix are byte-identical in both files, which is the part that has to be right.
# .github/workflows/build.yml
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- name: Restore or seed dependency cache
uses: WarpBuilds/cache@v1
with:
path: |
node_modules
.cache/turbo
key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('bun.lock') }}
restore-keys: |
deps-${{ runner.os }}-${{ runner.arch }}-
- run: bun install --frozen-lockfile
- run: bun run build# .github/workflows/test.yml
name: test
on: pull_request
jobs:
test:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v5
- name: Restore dependency cache
id: deps
uses: WarpBuilds/cache/restore@v1
with:
path: |
node_modules
.cache/turbo
key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('bun.lock') }}
restore-keys: |
deps-${{ runner.os }}-${{ runner.arch }}-
- run: bun install --frozen-lockfile
- run: bun run testThree details make this work.
The restore-keys prefix is what carries the sharing across lockfile changes. When bun.lock moves, the exact key misses, and the prefix deps-Linux-X64- matches the most recent entry written under that prefix instead. The job starts from a nearly correct dependency tree and bun install --frozen-lockfile reconciles the difference.
The runner sizes differ on purpose. build.yml uses warp-ubuntu-latest-x64-8x and test.yml uses warp-ubuntu-latest-x64-4x, and they still share, because machine size is absent from the cache address. Only the platform matters.
The install step is unguarded on purpose. Skipping it on a cache hit is tempting and wrong when the hit came through restore-keys, because a prefix match means the entry is stale by definition. Let the package manager reconcile.
Setup actions that handle the key for you
When the shared content is a language toolchain or a package manager directory, the key and the path list are both computed by the setup action, which means two workflows using the same action against the same lockfile path share automatically with no key management at all.
WarpBuild maintains cache-enabled forks of the common setup actions, listed in the setup actions documentation. They accept the same inputs as their upstream counterparts and route caching to WarpBuild Cache when they run on WarpBuild runners.
| Action | Replaces | What it caches |
|---|---|---|
WarpBuilds/setup-node@v6 | actions/setup-node | npm, yarn, and pnpm dependencies |
WarpBuilds/setup-python@v6 | actions/setup-python | pip, pipenv, and poetry dependencies |
WarpBuilds/setup-go@v6 | actions/setup-go | Go modules and build outputs |
WarpBuilds/setup-java@v5 | actions/setup-java | Maven, Gradle, and sbt dependencies |
WarpBuilds/rust-cache@v2 | Swatinem/rust-cache | Cargo registry, git dependencies, and target |
WarpBuilds/gradle-actions/setup-gradle@v5 | gradle/actions | Gradle User Home |
WarpBuilds/mise-action@v2 | jdx/mise-action | Tools installed by mise |
The catch is the same one as before, one level down. WarpBuilds/setup-node@v6 builds its key from cache-dependency-path, so two workflows that point that input at different lockfiles get different keys and do not share. Keeping the input identical across the workflow files is the whole requirement.
What a shared cache costs
Entries expire after 7 days without use, so the storage bill tracks the entries a repository actually touches. Rates below are from the pricing page and 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 |
Here is a month for a repository with three workflows sharing one entry. The assumptions are stated so you can substitute your own: 40 pull request pushes per weekday across 22 weekdays, which is 880 pushes; three workflows per push, each restoring the shared entry once; 120 merges to the default branch per month, each writing one entry; a 2.5 GB entry on the default branch held all month; and 30 feature branches that each write their own 2.5 GB entry and let it expire after 7 days.
| Line | Monthly quantity | Rate | Cost |
|---|---|---|---|
| Restores across three workflows | 2,640 operations | $0.0001 per operation | $0.26 |
| Writes from the default branch | 120 operations | $0.0001 per operation | $0.01 |
| Shared default-branch entry | 2.5 GB-months | $0.20 per GB-month | $0.50 |
| Per-branch entries, 7-day lifetime | 17.5 GB-months | $0.20 per GB-month | $3.50 |
| Total | $4.27 |
Set that against the runner minutes the shared entry removes. If a restore saves 2 minutes of dependency installation per job, the 2,640 jobs above skip 5,280 runner minutes, which on warp-ubuntu-latest-x64-4x at $0.008 per minute is $42.24 of runner billing that never happens. The per-branch entries are the sensitive line: dropping them by making pull request workflows restore-only, as test.yml above does, removes $3.50 of the $4.27 and leaves the sharing intact.
Two structural notes belong with any of these numbers. The persistent caches guide covers the cases where a whole-disk approach beats path-level caching, and what GitHub Actions cache storage costs works the storage arithmetic in more detail.
Related Questions
Can two jobs in the same workflow share a cache?
Yes, with one ordering rule. A cache entry becomes visible to other jobs only after the job that wrote it finishes its save step, so a job that needs the entry must declare the writing job in its needs list. Jobs that run in parallel with the writer will miss the entry and fall back to restore-keys. The share data between GitHub Actions jobs guide covers the fan-out shapes where that ordering gets expensive.
Should I use artifacts instead of a cache to pass files between workflows?
Use an artifact when a specific run's output has to reach a specific downstream run, such as a built binary going to a release workflow. Use a cache when the content is reproducible input that any run can rebuild, such as node_modules or a Cargo target directory. Artifacts are addressed by run and name, so they are the right tool for handoffs. Caches are addressed by key, version, and branch, so they are the right tool for reuse.
What does sharing a cache across workflows cost?
On WarpBuild hosted runners, cache storage is $0.20 per GB-month and each cache write, restore, or list is $0.0001 per operation. On BYOC both are free. Sharing one entry across three workflows costs three restore operations instead of three separate stored entries, so it lowers both lines. The pricing page carries the full rate sheet, and how long GitHub Actions caches last explains the 7-day expiry that sets the storage window.
Why does the same key miss on a different runner platform?
The cache version is a hash over the compression tool and the list of cached paths, and it is matched alongside the key. A macOS runner and a Linux runner produce different versions, so an entry saved on warp-macos-latest-arm64-6x cannot restore on warp-ubuntu-latest-x64-4x even when the key string is identical. The caching documentation describes the version hash and the cross-platform workarounds.
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.