Skip to content
Latchkey LogoLatchkey home

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.

The five segments of a rust-cache key, which inputs control each, and the three ways the post step ends
The restore key is the cache key minus its last segment. Read from src/config.ts, src/restore.ts and src/save.ts at v2.9.3 on 2026-11-12.

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.

InputDefaultWhat it does
prefix-keyv0-rustFirst key segment. Change it to start a fresh cache
shared-keyemptyReplaces key and the job id, so several jobs share one entry
keyemptyExtra segment added next to the job id
add-job-id-keytrueWhether GITHUB_JOB goes into the key
add-rust-environment-hash-keytrueWhether the environment hash and the lockfile hash go into the key
env-varsemptyExtra variable name prefixes to hash, on top of CARGO, CC, CFLAGS, CXX, CMAKE and RUST
workspaces. -> targetOne root -> target pair per line
cache-directoriesemptyExtra paths to cache
cache-targetstrueCache each workspace target; false keeps only the registry
cache-on-failureunset, read as falseRun the post step when the job failed
cache-all-cratesfalseKeep every crate in the registry, not only dependencies
cache-workspace-cratesfalseKeep your own workspace crates in target
save-iftrueWhether the post step saves at all
cache-providergithubgithub or warpbuild
cache-bintrueCache ~/.cargo/bin and its two install manifests
lookup-onlyfalseCheck 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.

.github/workflows/ci.yml
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 --locked

How 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.

.github/workflows/ci.yml
      - 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.

ReleaseChange
2.7.6cache-bin and lookup-only added, runner OS added to the key
2.7.8CPU architecture added to the key
2.8.0warpbuild provider and cache-workspace-crates added
2.9.0Runs on node24; every installed toolchain hashed; exact-hit check made case-insensitive
2.9.2buildjet provider removed; credentials.toml cleanup fixed
2.9.3Timestamp-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-if so only the default branch writes, and shared-key so 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?
Change 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?
Because the post step removes every file that was in ~/.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?
Usually because the install ran before the rust-cache step. The action records what is in ~/.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?
Not at v2.9.3: the restore key is always passed, so any entry for the same job, toolchain and environment can be restored. The 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.

References

More cores for the crates the cache cannot skip. Latchkey runners, by label. Start free → 30-day trial · No credit card