Swatinem/rust-cache: what goes into the key, and when to skip the save
Swatinem/rust-cache caches ~/.cargo and your target directory under a key made of the job, the runner OS and architecture, every installed rustc, the compiler environment variables and a parsed hash of your manifests and lockfile. It has 17 inputs at v2.9.3, and most workflows need three: shared-key, cache-on-failure and save-if.
You added Swatinem/rust-cache@v2 because a cold Rust build recompiles every dependency, and now you want to know what the inputs actually do, why a branch never seems to hit, or why the post step deleted something. The answers are in two files: action.yml for the inputs, and src/config.ts for the key, which is built from much more than your lockfile.
What the post step costs on a real workspace, and the order to check things when the cache feels slow, are on caching Cargo in GitHub Actions.

17 inputs, three that most jobs need
Inputs are strings, and the source compares most of them against the lowercase word true, so anything else reads as false. The one output, cache-hit, is true only on an exact match of the full key.
| Input | Default | What it does |
|---|---|---|
prefix-key | v0-rust | First key segment. Change it to start a fresh cache |
shared-key | empty | Replaces key and the job id, so several jobs share one entry |
key | empty | Extra segment added next to the job id |
add-job-id-key | true | Whether GITHUB_JOB goes into the key |
add-rust-environment-hash-key | true | Whether the environment hash and the lockfile hash go into the key |
env-vars | empty | Extra variable name prefixes to hash, on top of CARGO, CC, CFLAGS, CXX, CMAKE and RUST |
workspaces | . -> target | One root -> target pair per line |
cache-directories | empty | Extra paths to cache |
cache-targets | true | Cache each workspace target; false keeps only the registry |
cache-on-failure | unset, read as false | Run the post step when the job failed |
cache-all-crates | false | Keep every crate in the registry, not only dependencies |
cache-workspace-crates | false | Keep your own workspace crates in target |
save-if | true | Whether the post step saves at all |
cache-provider | github | github or warpbuild |
cache-bin | true | Cache ~/.cargo/bin and its two install manifests |
lookup-only | false | Check for an entry and set cache-hit without downloading |
cmd-format | {0} | Wrapper for rustc and cargo calls, such as nix develop -c {0} |
Where it goes in the job
Install the toolchain first. The restore step runs rustc -vV, and since v2.9.0 also rustup toolchain list and each toolchain's version, and hashes the result into the key. A toolchain installed after the action is not in the key, and the cache from a different compiler gets restored.
The restore step also exports CARGO_INCREMENTAL=0 into the rest of the job. Anything you cargo install should come after the action: the post step deletes the binaries that were already in ~/.cargo/bin when the action started, and keeps the ones the job added.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
shared-key: ci
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo test --lockedHow the key is built
The key is five segments joined with hyphens. First the prefix, v0-rust unless you change it. Second either your shared-key, or your key followed by the job id. Third the runner OS and CPU architecture as Node reports them. Fourth an 8-character SHA-1 digest of the rustc versions plus every environment variable whose name starts with one of the hashed prefixes. Fifth an 8-character digest of your manifests and lockfile.
The restore key is the same string without the fifth segment, and both are passed to the cache backend. A dependency change therefore misses the full key, hits the restore key, and gets last run's registry and target back. On that partial match the action cleans the target directory before your build and marks the entry to be saved again.
The manifest hash is parsed, not raw. Your own package version is rewritten to 0.0.0, path dependencies lose their path, and lock entries without a source or checksum are dropped, so a release commit that only bumps your version keeps the cache. rust-toolchain, rust-toolchain.toml and .cargo/config.toml files are hashed as plain files. A Cargo.lock in a format other than 3 or 4 falls back to a plain file hash with a warning.
Saving only from main
A cache entry created on a branch is readable from that branch only, while the default branch's entries are readable from every branch and from pull requests, per GitHub's cache scoping rules. A workflow where every branch saves fills the 10 GB default repository allowance with Rust targets that nobody else can read, and once the repository is over its limit, GitHub evicts entries in order of last access, oldest first.
save-if fixes that: every branch restores, only the default branch writes. With shared-key, a matrix of jobs on the same toolchain and environment restores one entry instead of one per job. If the matrix differs in features or targets, give each leg its own key instead, or the first leg to save wins and the others restore an entry built for something else.
The post step ends one of three ways. With save-if false it returns immediately. On an exact match it logs Cache up-to-date. and saves nothing. Otherwise it cleans the target directories, the registry, ~/.cargo/bin and the git checkouts, then saves under the full key.
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
shared-key: ${{ matrix.toolchain }}-${{ matrix.features }}
cache-on-failure: true
save-if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}What the post step deletes, and why
The cleaning exists because a raw Cargo target directory keeps every artifact of every dependency version it ever compiled, and an entry built from it only grows. The action keeps the build, .fingerprint and deps directories of each profile and, inside them, only entries for packages the current cargo metadata lists. Files older than a week are dropped on the partial-match pre-clean.
Two deletions are about safety rather than size. ~/.cargo/credentials.toml is removed before saving, so a registry token written by an earlier step never lands in a cache entry that every branch can restore. And the binaries that existed in ~/.cargo/bin before the action ran are deleted from disk, which is harmless on a fresh runner and is the known issue on a persistent self-hosted one.
Changes that shipped under the v2 tag
All of these shipped under the floating v2 tag, so a workflow on @v2 picked them up without a change. Pinning to a SHA is how you choose when.
| Release | Change |
|---|---|
| 2.7.6 | cache-bin and lookup-only added, runner OS added to the key |
| 2.7.8 | CPU architecture added to the key |
| 2.8.0 | warpbuild provider and cache-workspace-crates added |
| 2.9.0 | Runs on node24; every installed toolchain hashed; exact-hit check made case-insensitive |
| 2.9.2 | buildjet provider removed; credentials.toml cleanup fixed |
| 2.9.3 | Timestamp-based pruning fixed; no warning when rustup is missing |
Key takeaways
- Install the toolchain before the action, because every installed rustc is hashed into the key.
- The restore key drops only the lockfile hash, so a dependency change is a partial restore, not a cold build.
- Use
save-ifso only the default branch writes, andshared-keyso jobs on one toolchain share an entry. - An exact match saves nothing; the post step logs
Cache up-to-date.and returns.
Frequently asked questions
How do I clean the rust-cache cache or force a recache?
prefix-key, for example from the default v0-rust to v1-rust: every key changes, nothing old matches, and the next run builds and saves fresh. To delete what is stored instead, gh cache list --key v0-rust shows the entries and gh cache delete <key> removes one.Why do cargo and rustup get deleted when rust-cache saves?
~/.cargo/bin before the action ran, so that tools the runner image ships are not stored in the cache. On a fresh hosted runner that never matters. On a persistent self-hosted runner it deletes the real binaries, and the README's fix is cache-bin: "false".Why are crates installed with cargo install not cached?
~/.cargo/bin when it starts and treats those files as not yours, so they are deleted before the save. Install after the action with cache-bin left at its default and the binary, plus .crates.toml and .crates2.json, are kept.Is there an option to only use an exact cache hit?
cache-hit output does report only exact matches, so a step can branch on it. If partial restores are wrong for your build, use actions/cache directly with no restore-keys.