# Swatinem/rust-cache: what goes into the key, and when to skip the save

> Swatinem/rust-cache keys on rustc, env vars and parsed manifests. Learn all 17 inputs at v2.9.3, saving only from main, and what the post step prunes.

Source: https://latchkey.dev/learn/actions/swatinem-rust-cache  
Updated: 2026-11-12

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](/learn/speed/cache-rust-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}` |

> Read from `action.yml` and `src/config.ts` at v2.9.3 (commit d1527c8), 2026-11-12. `cache-directories` is described as newline separated but the source splits it on any whitespace, so a path containing a space becomes two paths. A `cmd-format` without exactly one `{0}` is silently replaced with `{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' }}
```

> Scope and the 10 GB default are from GitHub's dependency caching reference, read 2026-11-12. With `shared-key` set, `key` is ignored: the source adds `key` only in the branch where `shared-key` is empty. Put the feature name into `shared-key` itself when you need both.

## 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 |

> From CHANGELOG.md at v2.9.3, read 2026-11-12. The `buildjet` removal is not in the changelog: it is visible in `src/utils.ts` between the v2.9.1 and v2.9.2 tags, and a workflow still passing it now fails with: The `cache-provider` `buildjet` is not valid.

## FAQ

### 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

- [Swatinem/rust-cache action.yml at v2.9.3 (read 2026-11-12)](https://github.com/Swatinem/rust-cache/blob/v2.9.3/action.yml)
- [Swatinem/rust-cache src/config.ts: key construction](https://github.com/Swatinem/rust-cache/blob/v2.9.3/src/config.ts)
- [Swatinem/rust-cache CHANGELOG.md](https://github.com/Swatinem/rust-cache/blob/v2.9.3/CHANGELOG.md)
- [GitHub Docs: dependency caching reference](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
