Skip to content
rusthacks.dev
DevOps

Caching Cargo Builds in CI With Swatinem Rust Cache

Cache the Cargo registry and target directory in CI with Swatinem/rust-cache. Learn what is cached, the cache key, why caches go stale and how to keep restore times low.

3 min read
Summarize in AI
Flat illustration for the post “Caching Cargo Builds in CI With Swatinem Rust Cache”, composed around perf.

A cold Rust CI run compiles every dependency. A warm one compiles only your code. The difference is minutes against seconds, and one action provides it. The tuning is mostly about not defeating it: different flags in different jobs, caches that grow until the restore takes longer than the build, and pull requests that cannot see the main branch cache. This post is the caching step of GitHub Actions for Rust.

GitHub Actions, Swatinem/rust-cache v2, Rust 1.90.

The step

YAML
- uses: dtolnay/[email protected]
- uses: Swatinem/rust-cache@v2
  with:
    shared-key: "ci"
    cache-on-failure: true

It must run after the toolchain step, because the key includes the compiler version. shared-key lets several jobs (test, clippy, doc) use one cache instead of building the same dependencies three times. cache-on-failure saves the cache even when tests fail, so a failing build does not lose the work.

What it saves

On the post step it stores:

  • ~/.cargo/registry and git checkouts (downloaded crates)
  • target/ with the artifacts of dependencies
  • the version of rustc and hashes of Cargo.lock and Cargo.toml files in the key

Before saving, it deletes the artifacts of workspace members and other fast-changing files. Your own crates would never hit anyway, and including them is what makes naive actions/cache setups grow. Dependencies from git or paths are treated as workspace code when they change often.

Where caches go wrong

SymptomCauseFix
Never restoreskey changes each runcheck RUSTFLAGS and toolchain version are stable
Restores, still rebuilds alldifferent features or flagsuse the same flags in the jobs that share a cache
Restore takes 2 minutescache is hugeremove unused features, drop --all-features jobs from the shared key
PRs build coldno cache on the base branchrun the workflow on pushes to main
Cache evicted oftenrepository over the storage limitprune old keys, shrink the cache

RUSTFLAGS deserves emphasis: -D warnings in one job and none in another yields artifacts that Cargo treats as different, because flags are part of the fingerprint.

Make builds cheaper regardless of cache

  • Use cargo build --locked so the lock never changes during CI.
  • Set CARGO_INCREMENTAL=0 in CI; incremental data is large and does not help from a cold start. The action sets this by default.
  • Use a faster linker such as lld or mold on Linux.
  • Trim dependencies and features; compile time scales with them.

sccache

sccache wraps rustc and caches each compilation by hash, in a local directory, S3 or GitHub's cache. Set RUSTC_WRAPPER=sccache. It helps when many jobs or machines build the same crates, and when you need the cache outside GitHub. It does not cache linking, incremental builds or crates with build scripts that vary. For a single repository on GitHub, the action above is simpler.

Docker builds

Caching inside docker build is a different mechanism: layer caching, covered with cargo-chef, and cache mounts.

Limits

A cache cannot help the first build after a dependency bump, which compiles that dependency and everything that depends on it. If compile time remains the bottleneck, look at what is being compiled with cargo build --timings. The action's README documents the options: Swatinem/rust-cache.

Taggedcicache
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.