How Do I Cache Cargo Dependencies?
Cache the registry and git directories under Cargo home plus the target directory, keyed on Cargo.lock and the toolchain version. Paths, YAML, and costs.
Cache the two directories under Cargo home that hold downloaded dependencies, ~/.cargo/registry and ~/.cargo/git, plus the workspace target directory, and key the entry on a hash of Cargo.lock and the Rust toolchain version. The registry and git paths make a lockfile restore cheap, the target directory makes the compile cheap, and the toolchain in the key stops a rustc upgrade from restoring artifacts that Cargo will rebuild anyway.
Answer
Cargo splits its state across two places, and they behave differently, so they belong in one entry with one key but for different reasons.
The first is Cargo home, ~/.cargo unless CARGO_HOME says otherwise. The Cargo book's guidance on caching Cargo home in CI names the subdirectories worth storing:
| Path | Holds | Cache it |
|---|---|---|
~/.cargo/bin | Binaries installed with cargo install | Yes, when the workflow installs tools |
~/.cargo/registry/index | The registry index | Yes |
~/.cargo/registry/cache | Downloaded .crate archives | Yes |
~/.cargo/git/db | Bare clones of git dependencies | Yes |
~/.cargo/registry/src | Archives unpacked for compilation | No, re-extracted from registry/cache |
~/.cargo/git/checkouts | Checkouts of git dependencies | No, re-created from git/db |
Storing registry/src and git/checkouts adds a second, unpacked copy of every dependency to the entry, all of it regenerated from the two directories you already saved.
The second is target, the build cache that holds every compiled dependency, your own crates, test binaries, and incremental state. This is where the toolchain matters. Cargo fingerprints each artifact against the compiler that produced it, so a rustc upgrade invalidates the whole tree at once. A key built only from Cargo.lock hits after a toolchain bump, restores several gigabytes, and then rebuilds all of it, paying the download for nothing. Put the toolchain in the key and the bump starts a clean entry instead.
A working key looks like this:
${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}With restore-keys falling back to ${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-, a lockfile change starts from the previous mostly warm target rather than from nothing. Cache key covers the general shape of prefix fallbacks.
On WarpBuild runners the WarpBuild cache is a drop-in replacement for actions/cache@v4 and is enabled by default on Linux runners. The cache is not supported on the Windows runners, so a Windows Rust job keeps using GitHub's cache backend. Cache entries expire after 7 days without use, and the cache version is derived from the compression tool and the cached paths, so an entry written on a macOS runner does not restore on a Linux runner even under an identical key.
Detail
A workflow that restores, builds, and saves on success
The split restore and save actions give the save step a condition, which the combined action does not. That matters here: a build that fails partway leaves a target directory full of half-finished work, and saving it poisons every later run.
name: rust
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
build:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- name: Install toolchain
run: rustup toolchain install stable --profile minimal
- name: Restore cargo cache
id: cargo
uses: WarpBuilds/cache/restore@v1
with:
path: |
~/.cargo/bin
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
- run: cargo build --workspace --locked
- run: cargo test --workspace --locked
- name: Trim the target directory
if: success() && github.ref == 'refs/heads/main'
run: |
rm -rf target/debug/incremental
rm -rf target/debug/examples
find target/debug -maxdepth 1 -type f -delete
- name: Save cargo cache
if: success() && github.ref == 'refs/heads/main'
uses: WarpBuilds/cache/save@v1
with:
path: |
~/.cargo/bin
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
target
key: ${{ steps.cargo.outputs.cache-primary-key }}Three details carry the design. success() on the save step keeps a failed build from overwriting a good entry. The branch condition writes entries only from the default branch, which every pull request branch can restore through GitHub's branch scoping rules, so five active branches stop storing five near-identical copies. And cache-primary-key from the restore step is reused as the save key, so the two steps cannot drift apart when someone edits one of them.
If you would rather not maintain the paths yourself, WarpBuilds/rust-cache does this job with the same defaults and adds save-if for the branch condition and cache-on-failure, which stays false so failed runs save nothing. Install the toolchain before it runs, because the rustc version is part of the key it builds. The setup actions reference lists its inputs.
When the target directory outgrows the restore
target grows without bound. Every build writes new artifacts, superseded ones stay on disk, and incremental compilation adds a directory of per-module state that is large and worth nothing on a fresh machine. The entry grows with every merge, and past some size the restore step costs more wall-clock time than the compile it removes. GitHub's own backend hits a wall first: GitHub caps the combined size of all caches in a repository at 10 GB by default and evicts least recently used entries once the total passes it, checked on 2026-08-13.
Four measures keep the entry in useful territory:
- Set
CARGO_INCREMENTAL: 0for the workflow. Incremental state helps a developer machine that rebuilds the same tree repeatedly, and on a runner that sees each commit once it adds bulk with no reuse. - Delete
target/debug/incremental,target/debug/examples, and the loose binaries at the top oftarget/debugbefore saving. Those are your own crates, which recompile in seconds from cached dependencies. - Save only from the default branch, so superseded entries stop accumulating one per branch per lockfile bump.
- Read the restore step duration in your job log against the build step it replaced. When the two converge, drop
targetfrom thepathlist and keep caching Cargo home alone, which stays small and always pays.
For a target directory that stays useful at a size no cache wants to move, snapshot runners restore the whole runner disk instead of named paths, and CI observability shows which step the minutes actually go to. The persistent caches guide works through choosing between the two mechanisms.
What the cache costs
Both cache line items are published on the pricing page, checked on 2026-08-13:
| Line item | Hosted runners | BYOC |
|---|---|---|
| Cache storage | $0.20 per GB-month | Free |
| Cache write, restore, or list | $0.0001 per operation | Free |
A worked month, with the assumptions stated so you can substitute your own: a Rust workspace holding one 4 GB entry for the current lockfile and two superseded entries inside the 7 day window is 12 GB, which is $2.40 per month. At 40 workflow runs per weekday across 22 weekdays, 880 runs each performing one restore and, on the default branch only, one save is roughly 1,000 operations, which is $0.10 per month. The runner minutes dominate: warp-ubuntu-latest-x64-8x bills $0.016 per minute, so the same 880 runs at 6 minutes each is $84.48. Cache spend is the rounding error, and the reason to trim target is the restore seconds it costs every job rather than the storage bill.
Rust builds on GitHub Actions covers runner sizing for the same workloads.
Related Questions
Which Cargo directories should I cache?
Cache ~/.cargo/bin, ~/.cargo/registry/index, ~/.cargo/registry/cache, ~/.cargo/git/db, and the workspace target directory. The Cargo book names the first four as the Cargo home paths worth caching, because ~/.cargo/registry/src and ~/.cargo/git/checkouts are re-extracted from them on demand and only inflate the entry.
Why does my cargo cache miss after a Rust toolchain upgrade?
Artifacts in target are fingerprinted against the compiler that produced them, so a new rustc rebuilds everything the old one compiled. Put the toolchain version in the cache key with hashFiles('rust-toolchain.toml') or a hash of rustc -vV output, so a toolchain bump starts a new entry instead of restoring dead artifacts. Rust incremental compilation on GitHub Actions covers what survives a rebuild and what does not.
Should I cache the target directory at all?
Cache it while restoring it takes less time than the compile it removes. Compare the restore step duration in the job log against the build step it saved, set CARGO_INCREMENTAL: 0, delete target/debug/incremental before saving, and drop target from the entry once the numbers cross. Cargo home alone stays small and keeps paying, and the persistent caches guide covers this same tradeoff for other large, slow-to-rebuild directories.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.