# Cache Cargo GitHub Actions builds with rust-cache, and its post step

> Cache Cargo GitHub Actions builds with Swatinem/rust-cache: what the two keys do, what the post step deletes, and why it can take minutes.

Source: https://latchkey.dev/learn/speed/cache-rust-cargo-in-github-actions  
Updated: 2026-09-21

To cache Cargo GitHub Actions builds the answer is `Swatinem/rust-cache`, and the two things worth understanding about it are both invisible from the workflow file: it restores on a partial key when the exact one misses, and before it saves anything it deletes most of what your build just produced. That deletion is why the post step sometimes takes longer than the build.

A Rust CI job is slow for one reason: everything compiles from source, including hundreds of dependency crates that have not changed since the last run. Caching the registry and the `target` directory is the highest-leverage change available, and `Swatinem/rust-cache` is the action that does it with keys and pruning nobody has to maintain.

It is also an action with strong opinions that it does not announce in its README summary. It exports `CARGO_INCREMENTAL=0` into your job. It refuses to save on a failed build unless you ask it to. It deliberately does not cache your own workspace crates by default. And its post step runs four cleanup passes before the upload. Each of those is defensible and each surprises somebody once a quarter.

## Two keys, not one, and what that buys

The action builds a cache key and a restore key, and hands both to the toolkit. The cache key is the full one: a `v0-rust` prefix, then either your `shared-key` or your `key` plus the job name, then the runner OS and architecture, then a hash of the Rust environment, then a hash of your manifests and lockfile. The restore key is the same string with the last piece removed, so it matches any recent cache for the same job, toolchain and environment regardless of which dependencies were in it.

That is the difference between this action and a cache on an exact key. When you add one dependency, the full key misses and the restore key hits, and you get last run's registry and last run's compiled `target` back. Cargo then builds the delta. The action notices the partial match, cleans the target directory of anything stale before the build starts, and marks the entry for re-saving at the end.

The lockfile hash is more careful than a plain file hash, which matters on a workspace. The action parses `Cargo.toml` and `Cargo.lock`, rewrites your own package version to `0.0.0`, blanks the path of any path dependency, and drops lock entries with no source or checksum. The effect is that bumping your own crate version does not invalidate the whole cache, which a naive `hashFiles` over those files would do on every release commit.

| Key component | In the cache key | In the restore key |
| --- | --- | --- |
| `v0-rust` prefix, or your `prefix-key` | Yes | Yes |
| `shared-key`, or `key` plus the job id | Yes | Yes |
| Runner OS and CPU architecture | Yes | Yes |
| Rust versions and the CARGO, CC, CFLAGS, CXX, CMAKE and RUST environment variables | Yes | Yes |
| Hash of parsed manifests and `Cargo.lock` | Yes | No |

> Read from `src/config.ts` in `Swatinem/rust-cache` on 21 September 2026. `shared-key` is the input to reach for when several jobs should share one entry: it replaces the job id in the prefix, so a `build` job and a `clippy` job can restore each other's work instead of keeping two copies of the same dependency tree.

## What actually gets cached

Five paths by default, and one of them is conditional. The registry and the git checkouts under `CARGO_HOME` are always included. The `bin` directory and the two crate manifests beside it are included unless you set `cache-bin: false`, which is how a cargo tool you installed in an earlier step survives to the next run. Each workspace `target` directory is included unless you set `cache-targets: false`, which leaves you with a registry cache and a full recompile.

What is deliberately excluded is your own code. `cache-all-crates` defaults to false, so only crates your workspace depends on are kept in the registry, and `cache-workspace-crates` defaults to false, so the compiled artifacts of your own workspace members are removed from `target` before the save. That is the right default for correctness and it is the thing people turn off when they measure the post step, because it is a large part of what the cleanup is doing.

```.github/workflows/ci.yml
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
  with:
    # one entry shared by every job in the workflow instead of one per job
    shared-key: ci
    # keep an entry from a red build, so a flaky suite still warms the cache
    cache-on-failure: true
    # only the default branch writes; feature branches restore
    save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo test --all
```

> `save-if` is the input that fixes a repository whose cache entries evict each other. With one writer on the default branch you keep one good entry instead of a dozen branch-scoped ones competing for the same 10 GB, which is the usual cause of a Rust cache that seems to work and then stops.

## Why the post step takes minutes

Before it uploads anything, the save step runs four passes in order: it cleans each workspace target directory, cleans the cargo registry, cleans `cargo/bin`, and cleans the cargo git cache. Only then does it archive and upload. On a small crate you will never notice. On a workspace with a few hundred dependencies the cleaning is a large number of filesystem operations over a directory tree that is itself a few gigabytes, and it can take longer than the build it is caching.

The cleaning is not gratuitous. A Cargo target directory accumulates every artifact from every version of every dependency it has ever compiled, and caching it raw would produce an entry that grows without limit and evicts everything else in the repository. The passes keep the `build`, `.fingerprint` and `deps` directories and, inside them, only the entries belonging to packages the current build actually used.

If your post step is the thing hurting, the levers are in this order: set `cache-targets: false` to cache the registry alone, which removes the largest cleaning pass entirely; set `save-if` so only one branch pays for the save; or split the workspace so each job caches a smaller target directory. Turning the action off is rarely the answer, because a cold Rust build is nearly always worse than a slow save.

```Actions log, post step, quoted from Swatinem/rust-cache#215
... Cleaning /home/runner/actions-runner/_work/rsmono/rsmono/target ...
... Cleaning cargo registry (cache-all-crates: false) ...
... Cleaning cargo/bin ...
... Cleaning cargo git cache ...
... Saving cache ...
/usr/bin/tar --posix -cf cache.tzst --exclude cache.tzst -P -C /home/runner/actions-runner/_work/rsmono/rsmono --files-from manifest.txt --use-compress-program zstdmt
```

> Those six lines are the whole post step in order, and they are what to read when it is slow: the four cleaning passes, then the archive. The `cache-all-crates: false` in the second line is the action printing its own input back, which is how you confirm from a log which pruning mode a run used. The issue they come from is titled "Slow post job cleanup", which is the failure mode rather than a bug.

## The environment variable it sets for you

The restore step exports `CARGO_INCREMENTAL=0` into the rest of the job. That disables Cargo incremental compilation, and it is correct for CI for two reasons: incremental artifacts are large, so they would bloat the cache badly, and they are useless across runners because the next job is a fresh machine anyway.

It has one visible consequence worth knowing. `CARGO_INCREMENTAL` is one of the variables hashed into the cache key, along with everything else starting with CARGO, CC, CFLAGS, CXX, CMAKE or RUST. So a workflow that sets `RUSTFLAGS` differently on two jobs has two different cache keys by design, and a workflow that changes `RUSTFLAGS` at all invalidates its cache. That is the correct behavior, and it is also the reason a one-character change to a lint flag can produce a completely cold build.

If you need extra variables in the key, `env-vars` takes a space-separated list of additional prefixes. If you need fewer, there is no way to remove the defaults, and the right fix is to stop setting a variable that differs per run.

## What to check, in order

- Read the Cache Configuration group in the restore step. It prints the cache key, the restore key, the exact environment variables considered and the lockfiles considered, which answers most questions without changing anything.
- Look for `full match: false` in that step. That is a partial restore working as designed, not a problem, and it means the delta is being compiled rather than the whole tree.
- If nothing ever restores on a feature branch, check that something runs on the default branch. Cache scoping means a branch can only read its own entries, the default branch and, on a pull request, its base.
- If the post step is slow, time it against the build before you change anything, then try `cache-targets: false` and compare. Caching the registry alone is often most of the win for a fraction of the save.
- If entries seem to vanish, add up what you are storing. A Rust target cache is easily several gigabytes and the repository allowance is 10 GB, so two or three branch-scoped entries evict each other. [GitHub Actions cache size limit](/learn/speed/github-actions-cache-size-limit) has the eviction rules.

## FAQ

### Why is the rust-cache post job cleanup so slow?

Because it prunes before it uploads. The save step cleans each workspace target directory, the cargo registry, `cargo/bin` and the cargo git cache, then archives what is left. On a workspace with hundreds of dependencies that is a very large number of filesystem operations over a multi-gigabyte tree, and it can exceed the build time. Setting `cache-targets: false` removes the largest pass.

### What does Swatinem/rust-cache actually cache?

The cargo registry and git checkouts under `CARGO_HOME`, the `CARGO_HOME/bin` directory with its two crate manifests unless `cache-bin` is false, and each workspace `target` directory unless `cache-targets` is false. Your own workspace crates are pruned out of both the registry and the target directory by default, which is what `cache-all-crates` and `cache-workspace-crates` control.

### Does rust-cache save the cache when the build fails?

Not by default. Its post step runs on `success()` unless `cache-on-failure` is true, in which case it also runs when the job failed. Turning it on is usually right for a repository with a flaky suite, because otherwise half your runs contribute nothing and the cache stays cold much longer than the dependency churn alone would explain.

### Why did my Rust cache go cold after a RUSTFLAGS change?

Because the environment is hashed into the key. The action hashes the Rust versions plus every variable whose name starts with CARGO, CC, CFLAGS, CXX, CMAKE or RUST, so changing `RUSTFLAGS` by one character produces a different key and a different restore key. That is deliberate: artifacts compiled under different flags are not interchangeable, so reusing them would be worse than recompiling.

### How do I share one Rust cache across several jobs?

Set `shared-key` to the same value on each job. By default the key includes the job id, so a `build` job and a `clippy` job keep separate entries of the same dependency tree and compete for the same repository allowance. A shared key replaces the job id in the prefix, so the second job restores what the first one saved.

## References

- [Swatinem/rust-cache: key and path construction in src/config.ts (verified 2026-09-21)](https://github.com/Swatinem/rust-cache/blob/master/src/config.ts)
- [Swatinem/rust-cache: the cleaning passes in src/save.ts and src/cleanup.ts (verified 2026-09-21)](https://github.com/Swatinem/rust-cache/blob/master/src/save.ts)
- [Swatinem/rust-cache#215, the post step log quoted on this page (verified 2026-09-21)](https://github.com/Swatinem/rust-cache/issues/215)
- [GitHub Docs: dependency caching reference, key matching and cache scope (verified 2026-09-21)](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
