In my last post, I described how we share one Cargo build directory between git worktrees. I run several coding agents at once, each in its own worktree, and every worktree that built our Rust Lambda workers compiled the same 400 or so dependencies into its own target/ folder. Pointing them all at one build directory brought each extra worktree down from 8.2 GB to 2.7 GB.
It worked, but it took some hacks. Cargo gives our own crates the same file names in every worktree, so I had to give each worktree's crates a different hash with an unstable Cargo feature, turned on with a variable from Cargo's own test suite. Every worktree also waited on the build directory's one lock.
On r/rust, LankyLingonberry2532 asked why we didn't use kache. I hadn't heard of it, so I tried it. It took one change to our Cargo config to get it working, and now a new worktree's builds take 14 seconds and add 2 GB, on stable Cargo with nothing shared but the cache.
What kache does #
kache is a compiler cache, like sccache: Cargo runs rustc through it, and it keys each compile by the content of its inputs (the source files, the compiler, the flags, the dependencies' outputs). The difference is what it does on a cache hit. sccache copies the cached files into the worktree's target/, so every worktree still holds its own copy of every dependency. kache stores each output once and restores it as an APFS clone: a copy-on-write file that shares its disk blocks with the cache until one of them is changed.
Each worktree keeps its own target/ folder and its own fingerprints, which is how Cargo decides what's up to date. That rules out the bug from my last post, where one worktree reused another's build of our crate and ran the other branch's code. Builds don't share a lock either: two worktrees can build at the same time, and when both need the same crate, kache compiles it once.
Setting it up #
cargo install kache --locked
kache init
kache init shows what it will change before it changes anything. It sets kache as Cargo's rustc-wrapper in ~/.cargo/config.toml, starts a background daemon (with a launchd service on macOS), and adds a few lines to your shell startup files. Those put a directory of shims in front of your PATH: cargo and rustdoc, which run the real tools through kache, and cc, gcc, clang, c++ and g++, which cache C and C++ compiles. It also sets kache test-runner as Cargo's test runner, so test binaries wait for the CPU slots that builds use.
I deleted the C and C++ shims. They would send every C compile on my machine through kache, including Ruby and Node's native extensions, and I only wanted it for Rust. I kept the cargo and rustdoc shims, because kache uses them to fill a new target/ folder before Cargo looks at it.
I had to change two more things. My shell exported RUSTC_WRAPPER=sccache, which overrides Cargo's config, so I removed that and set sccache as kache's fallback for anything kache doesn't cache:
kache config set cache.fallback sccache
And since our coding agents run the builds, I added a note to their global instructions: cargo is a kache shim, du overstates a target/ folder's size (more on that below), and KACHE_DISABLED=1 runs a build without kache when something looks wrong.
Our test builds didn't share anything at first #
Each of our worktrees makes two builds: the test build, and a plain build of the document worker that our Rails app runs in development. I tested kache with fresh copies of our repository, the same way I measured the shared build directory.
The document worker build worked straight away. In a second copy, every crate came out of the cache, and the build took 8 to 10 seconds. The test build didn't improve at all: each copy compiled almost everything again, and took about as long as a build with an empty cache.
kache explain serde showed what was happening: the cache held six entries for the same serde, with the same version, features and source, one for each copy. Their keys differed in the checkout's path. Our test build's Cargo config added coverage instrumentation to every crate:
[target.'cfg(all())']
rustflags = ["-C", "instrument-coverage"]
Coverage instrumentation writes source file paths into the compiled output, so kache can't reuse an instrumented crate in a checkout at another path, and it puts the path into the key. That's the right thing for kache to do, and it meant no instrumented crate could be shared.
We didn't need that config at all. Our coverage reports come from cargo-llvm-cov, which adds its own instrumentation flags. The config only instrumented every plain cargo test and cargo build, and our image builds, CI jobs and document worker script all had to turn it off again. So I deleted it, along with the workarounds, and checked that cargo llvm-cov still produces its report.
If kache isn't sharing your builds, look for a flag like this in your Cargo config. kache explain <crate> shows which part of the key changed.
The numbers #
I measured these on an M2 Pro MacBook with our Lambda workers, Cargo 1.99.1 and kache 2.1.1. Each worktree makes both builds. The first two rows are from my last post.
| Each worktree after the first | Disk | Time |
|---|---|---|
| Separate build folders | 8.2 GB | 99 seconds |
| Shared build directory | 2.7 GB | 22 seconds |
| kache | 2.0 GB | 14 seconds |
With kache, a second worktree's test build ran rustc 16 to 52 times and compiled one crate, our own. kache had already filled its target/ folder with the dependencies from the cache, so Cargo found them up to date. The document worker build compiled nothing.
The first worktree pays for the cache. With an empty cache, its test build took 236 seconds and its document worker build 37 seconds, since kache filled the document worker's target/ folder from the test build's dependencies.
Ten worktrees would take about 31 GB with kache: about 13 GB for the first, whose builds the cache shares its blocks with, and 2 GB for each one after it. That's close to the shared build directory's 33 GB, against 82 GB with separate build folders. Most of the gain is in time.
I also checked that an edit in one worktree only reaches that worktree. I changed the document worker's main in one copy and rebuilt all three: only that copy's binary printed the change.
What to watch out for #
du and Finder show each target/ folder at its full size, 12 GB for one of ours, because they count the cloned blocks in every folder. Measure what a worktree really costs with df before and after its build.
The cache has a size budget: by default 5% of the disk, at least 5 GB and at most 100 GB, and kache removes old entries when it's over. kache clean --stale 14d also removes target/ folders that no build has used for two weeks, or whose worktree is gone.
kache is young. The repository is from February 2026, version 1.0 came out in October, and 2.0 at the start of November. A cache like this has to put every input of a compile into its key, and a missed input would give you a stale build. That's the same kind of failure as the bug in my last post, and I haven't seen it, but if a build ever does something you can't explain, run it again with KACHE_DISABLED=1. kache doctor checks the setup, and kache explain shows why a crate missed the cache.
I've only tried kache on macOS, and our CI doesn't use it. On Linux, it uses copy-on-write clones on filesystems that support them, such as Btrfs and XFS, and otherwise hardlinks or copies.
What we removed #
Moving to kache let us delete the script that wrote each worktree's Cargo config, the script that deleted the shared directory when it grew past 50 GB, the unstable feature and Cargo's test-suite variable, and the rust-analyzer setting that passed the variable to my editor. Removing the coverage config also removed a workaround from five image builds, two CI jobs and our document worker script.
The Cargo team is still working on a build cache shared between workspaces, and it's one of the Rust project's goals for 2026. Until then, we're using kache.