Skip to content
compiler.dev

Cache Cargo and Rust Builds in GitHub Actions

Last updated

To cache Cargo in GitHub Actions, the simplest option is Swatinem/rust-cache. It caches the Cargo registry, git dependencies and the compiled dependencies in target, and it trims the cache before saving. The explicit actions/cache form works too, but caching target blindly gets huge and slow.

Option 1: Swatinem/rust-cache

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --locked

According to the rust-cache README (checked October 2026), the action caches ~/.cargo (installed binaries, the registry index and crate cache, and git dependencies, with registry/src left out) and ./target. It does not cache your own workspace crates, only their dependencies. The key is built from the job name, the Rust version, and the hash of Cargo.lock and manifest files, and it sets CARGO_INCREMENTAL=0.

Before saving, it removes anything that is not a current dependency, incremental artifacts, and build artifacts older than a week. That cleanup is the main reason to prefer it over a hand-written cache.

Inputs you will actually use:

- uses: Swatinem/rust-cache@v2
  with:
    workspaces: |
      . -> target
      tools/xtask -> target
    shared-key: ci
    save-if: ${{ github.ref == 'refs/heads/main' }}
    cache-on-failure: true
  • workspaces lists workspace -> target pairs for repos with more than one Cargo workspace.
  • shared-key makes several jobs share one cache instead of one per job name. Good when check, clippy and test build the same dependencies, though debug and release profiles differ, so share only between jobs with the same profile.
  • save-if saves only from main, keeping PR branches from filling your quota.
  • cache-on-failure saves even if the job fails, useful because a failing test run has still compiled all dependencies.

Option 2: explicit actions/cache

- uses: actions/cache@v6
  with:
    path: |
      ~/.cargo/registry/index/
      ~/.cargo/registry/cache/
      ~/.cargo/git/db/
      target/
    key: cargo-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }}
    restore-keys: |
      cargo-${{ runner.os }}-${{ runner.arch }}-
- run: cargo build --locked

This is the pattern Cargo's own guidance and the actions/cache examples use. The key has OS, architecture and Cargo.lock. Add the compiler version (rustc --version) if you do not pin a toolchain with rust-toolchain.toml, since artifacts built by one rustc are not reused by another.

What to cache, and what not to

PathCache?Reason
~/.cargo/registry/index and registry/cacheYesAvoids re-downloading crates
~/.cargo/git/dbYesGit dependencies
~/.cargo/registry/srcNoUnpacked sources; Cargo recreates from cache quickly
target/ dependenciesYesThe big win: compiled dependency crates
target/ of your own cratesNoChanges every commit and bloats the cache
target/**/incrementalNoLarge and rarely useful in CI
~/.cargo/binCarefulCache installed tools by version, not wholesale

The "rolling cache" trap: with an explicit key on Cargo.lock, the cache is only saved when the lockfile changes. Your workspace crates are never saved, which is what you want. If you instead key on github.sha, the cache grows with every commit's build output. rust-cache avoids this by cleaning before save.

When target is too big

A Rust target directory for a large workspace can be several GB, and restore time grows with it. Options:

  1. Turn off debug info for CI: CARGO_PROFILE_DEV_DEBUG=0 (or debug = "line-tables-only"). It shrinks artifacts and speeds linking.
  2. Use sccache as a compiler wrapper with the GitHub Actions cache backend instead of caching target. It caches per-crate compile results rather than whole directories.
  3. Cache only ~/.cargo and accept rebuilding dependencies. Often worse, but cheaper on quota.
  4. Run on more cores. Compile time scales with CPU; see faster runners. In the compiler.dev measured run for React plus Rust (Axum) with Postgres, full CI went from 2m37s to 1m45s (see benchmarks).

Other speed-ups that matter more than the cache

  • cargo test --locked fails if Cargo.lock is out of date, so CI never resolves silently.
  • cargo nextest runs tests in parallel across binaries; pair it with test sharding for large suites.
  • Use cargo check for lint-only jobs. It skips code generation.
  • mold or lld as the linker cuts link time for big binaries.

How to verify a cache hit

  1. rust-cache logs Cache restored from key: ... (look for Restoring cache and the key it matched) and a post-step summary of what it saved or why it did not.
  2. In build output, a warm run shows Compiling only for your crates. A cold run lists every dependency.
  3. Compare the time of cargo build between runs with the same lockfile.
  4. gh cache list --key rust --limit 20 shows size and last-used.

Common mistakes

  1. Different jobs fighting over one key. Debug and release builds, or clippy and test, invalidate each other. Use per-profile keys.
  2. Not using --locked. A lockfile that drifts silently changes your key's meaning.
  3. Caching the whole target from every PR. PR caches are not shared across PRs; save from main. See Caching in GitHub Actions.
  4. Hitting the quota. Rust caches are large. Read cache limits.

FAQ

Should I cache the target directory?

Cache dependency artifacts in it, yes, and let a tool like rust-cache remove your own crates' output. Caching everything grows without bound.

Is sccache better than rust-cache?

They solve different problems. rust-cache restores a directory; sccache caches individual compile units and works across branches and profiles. For large workspaces sccache can win; for most projects rust-cache is simpler.

Why does CARGO_INCREMENTAL=0 matter?

Incremental compilation writes large artifacts that help local rebuilds but are of little use on a fresh CI machine.

How do I cache cargo install tools?

Key on the tool and its version, or use a prebuilt-binary action such as taiki-e/install-action.

Next steps

Related reading: speeding up Rust CI end to end, Go modules caching for a similar two-directory pattern, and how to speed up GitHub Actions. To try Rust CI on 8 vCPU machines, see compiler.dev pricing.