Skipbox

Why rust-cache doesn't make your Rust CI fast (tauri: 246 s to 53 s)

Swatinem/rust-cache, the cache step in most Rust CI, saves your dependencies and deliberately leaves out your workspace crates. It also sets CARGO_INCREMENTAL=0. On GitHub's runners every job starts on a new VM, so every warm job, full cache hit and all, compiles your own crates again. No rust-cache setting makes that go away. What does is a target/ still there from the last job: Cargo then skips every crate that hasn't changed.

Skipbox keeps it for you. Your job runs on your account's own machine, which keeps the checkout and Cargo's target/ between jobs. tauri's CI job took 246 s on GitHub and 53 s there, compiling none of its crates from the first warm run, with no change to its workflow but runs-on: skipbox, for a flat $39 a month.

What rust-cache keeps, and what it leaves out

From rust-cache's README and its source:

  • What it caches: ~/.cargo (the registry, git dependencies, installed binaries) and ./target with your dependencies' build output.
  • What it leaves out: "the workspace crates themselves are not cached". Before the save, its cleanup deletes everything in target/ that isn't a dependency, so your crates' artifacts are removed from target/ itself.
  • Incremental compilation: the restore step exports CARGO_INCREMENTAL=0 for the rest of the job.
  • When it saves: only when no cache has the job's key yet. The key comes from the toolchain, the environment, and your Cargo.lock and Cargo.toml. Your source files aren't part of it.

On a fresh VM that adds up to one thing. A warm job restores your dependencies, then compiles your workspace crates and everything built on them, whole, every time.

The mechanism, in tauri's logs

tauri's test job runs five cargo test steps on Ubuntu, with rust-cache. Here are its warm runs, each a full cache hit (full match: true in the log):

The 32 lines are tauri's own crates, and two plugins built on them. The dependencies never show up: they come from the cache. Cargo's build time is the Finished … in times of the five steps, added up.

The fresh VM has the own machine's CPUs and the same cache, and it compiled the same 32 crates as GitHub: its target/ held only what the cache restored. On the own machine, the first warm run already prints no Compiling line. Cargo only checked that nothing had changed, and each Finished took 0.2-0.7 s.

The own machine's first warm run used to compile all 32 crates again (a 114 s job), because rust-cache's save after the cold run had deleted them from target/. Since 13 November 2026, own machines undo what rust-cache's restore and save delete from the disk, once the step ends. The cache it saves is the same, and your crates stay in target/.

Job times: three Rust projects, seven others

Real projects' own CI jobs at a pinned commit, the steps byte for byte; only the trigger and runs-on changed. One cold run, then two warm ones, on each side:

  • GitHub: ubuntu-24.04, the standard runner of a public repository: 4 vCPU, 16 GB.
  • Skipbox, fresh VM: a new VM per job from the production runner image, 4 vCPU and 8 GB, caches restored through Skipbox's cache.
  • Skipbox, own machine: the same size on the same host CPUs, kept between jobs, with the keep-and-wipe that own machines run since 13 November 2026: the checkout and target/ stay, and what rust-cache deletes is put back.

The Skipbox runs were on a Skipbox host, in a lab project kept apart from customers' machines. Times run from each job's start to its end, from GitHub's API. Each figure is the median of the two warm runs. The own machine's cold runs took 285 s for tauri, 161 s for ripgrep and 442 s for helix.

ripgrep builds inside cross's Docker image. cross makes target/ itself and tags it "created by cross". Own machines used to take that target/ for something other than Cargo's and delete it after every job, so Cargo compiled every crate again on every run (a 108 s job). Now cross's target/ stays: the warm runs print no Compiling line, and each Finished takes 0.04-0.05 s. GitHub also downloads cross's image on every run (30 s in the second warm run); the own machine keeps it on disk.

helix is still 1.5×, because most of its job is testing, not building. Its integration test suite alone runs for 112 s. The only build left is helix-term (and xtask, which depends on it): helix-term's build script runs again every time, which costs 16 s in the test profile and 61 s in the integration profile. No other crate compiles. Its setup step, toolchain and cache restore, took 9-10 s against 41-46 s on GitHub.

The other languages:

astro is the same story as target/, with Turborepo: the own machine keeps turbo's cache (turbo 2), so its warm runs replay all 40 build tasks and the test task from it instead of running them (31 s, 28 s).

mastodon's image build takes every layer from the type=gha cache on both sides, so there was nothing left to win. vite's own-machine figure is its one warm run that passed.

A private repository's ubuntu-latest has 2 CPUs and 8 GB: half the cores of the GitHub runs above.

Check your own CI

  1. Open a warm run of your Rust job, one whose rust-cache step logs full match: true.
  2. In the build and test steps, count the Compiling lines. Dependencies from the cache print none. Each of your workspace crates prints one, and so does each crate built on them.
  3. Add up the steps' Finished … in times. That is what your own crates cost each run, and what a kept target/ saves when they haven't changed.

What you can do on GitHub's runners

  • cache-workspace-crates: true (rust-cache v2.8.0 and later). Your crates go into the cache too. But a run whose key exists skips the save, and the key doesn't change with your source, so the cached crates are as old as the last save. Incremental compilation stays off, and the cache grows inside GitHub's 10 GB per repository.
  • sccache. It caches each compilation by its inputs, so an unchanged library crate comes back from storage. Crates that invoke the linker can't be cached: binaries, dylib, cdylib and proc-macro crates compile every time, and every hit is a download.
  • A self-hosted runner that keeps target/. You run the machine: patch it, clean it between jobs without deleting target/, and set clean: false on actions/checkout, whose default git clean -ffdx deletes target/ with every other ignored file.

Skipbox keeps target/ for you

Each plan comes with your account's own machine, kept between jobs. Since 13 November 2026 it keeps your checkout, with its files' times, and Cargo's target/:

  • actions/checkout's clean runs as usual, except that it leaves Cargo's target/ dirs alone.
  • rust-cache stays in your workflow. Its restore no longer writes the cache's older artifacts over the newer ones on disk.
  • What rust-cache's save and restore delete from target/ and ~/.cargo, the machine puts back once the step ends. The cache saved is the same, and from the job after the cold run, crates that haven't changed aren't compiled.
  • The target/ that cross makes (tagged "created by cross") stays too.
  • actions/checkout rewrites .git/HEAD on every job. While it still names the same commit, the file gets its old time back, so a build script that watches .git/HEAD (vergen does) doesn't rerun for that alone.

The machine keeps Docker images, package caches and installed tools too, so the rest of the job speeds up with it. A job picks its size by label:

Minutes are unlimited. At 20,000 minutes a month on 4 vCPU, GitHub bills $222; Skipbox S is $39, $183 less.

Moving is one line

jobs:
  test:
    runs-on: skipbox # was ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --workspace
  1. Sign in to Skipbox with GitHub.
  2. Install the Skipbox GitHub App and pick the repositories.
  3. Change runs-on and push.

Security

  • The machine is your account's alone. No other company's jobs run on it.
  • After every job, the runner's credentials, processes and containers are wiped, and so are the git credentials, hooks and untracked files in the kept checkout. Clean every job gives each job a fresh machine instead.
  • Code from people without write access (such as pull requests from forks) runs on a fresh, throwaway machine, with no kept target/.

The details are in how jobs are isolated.

Try it on your Rust job

The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move your Rust job to runs-on: skipbox, run it twice, and put the second run's Compiling lines next to GitHub's.

FAQ

Does Swatinem/rust-cache cache my workspace crates?

No, not by default. Its README says the workspace crates themselves are not cached, and its save step deletes their build output from target/ before it uploads the cache. It caches ~/.cargo and your dependencies' build output, and sets CARGO_INCREMENTAL=0 for the rest of the job. So on a fresh GitHub VM, every warm run compiles your workspace crates again, even on a full cache hit. In tauri's CI on GitHub that was 32 Compiling lines on every warm run.

Should I set cache-workspace-crates: true?

It puts your crates in the cache too (rust-cache v2.8.0 and later), but the cache key comes from your toolchain, your environment and your Cargo.lock and Cargo.toml, not your source. A run whose key already exists skips the save, so the cached copy of your crates is as old as the last save and drifts from your code with every commit. Incremental compilation stays off. Check a warm run's log: if your crates still show Compiling, it isn't helping.

Does sccache fix slow Rust CI on GitHub Actions?

It helps with library crates whose inputs haven't changed. sccache can't cache crates that invoke the linker (bin, dylib, cdylib and proc-macro crates), and every hit is a download from its cache storage. A target/ kept between jobs needs no download: a crate Cargo sees as unchanged isn't compiled at all.

Do I have to change my workflow to keep target/ on Skipbox?

No. Change runs-on to skipbox and leave rust-cache and actions/checkout as they are. Your own machine keeps the checkout and Cargo's target/ between jobs, checkout's clean leaves the target/ dirs alone. When rust-cache's save deletes your crates from target/, the machine puts them back, and its restore no longer writes older artifacts over newer ones. So from the first warm run, crates that haven't changed aren't compiled.

How much does it cost to try?

Nothing. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move your Rust job to runs-on: skipbox, run it twice, and compare the second run's Compiling lines and build steps with GitHub's.

Updated November 13, 2026

Faster CI/CD with an always-warm cache

Your warm runner at a flat price

1/12 the cost of GitHub at full use. Flat price, so run it all day. 7× GitHub's cache space.

  • No card
  • Sign in with GitHub
  • Cancel anytime