kache 0.3.0: C/C++ build caching, crates.io, and the storage win
Benchmarks, C/C++ caching, Windows, an easier install: you asked, and kache 0.3.0 answers.
When we open-sourced kache, we expected it to be a quiet release of an internal tool.
Instead, a lot of people reached out. They sent feedback, surfaced use cases we hadn't thought about, and made requests that genuinely changed our roadmap. kache 0.3.0 is the first release shaped largely by that feedback, and this post walks through what landed.
Short version:
- C/C++ caching arrives, behind a deliberately conservative allow-list
- A fallback wrapper catches the compiles kache skips,
sccacheincluded - crates.io publishing is on the way, now that the
kachename is being transferred to us - Windows support is underway, with the cross-platform groundwork in place
- Restores got cheaper with reflink-first materialization
- Setup got simpler with
kache init
Speed is nice. Storage is the quiet win.
The most common request has been benchmarks. Fair enough. We're working on a proper, reproducible benchmark suite, and we'd rather publish numbers we trust than numbers that look good.
But the framing itself deserves a second look. Build caching is almost always discussed as a speed problem. kache is faster, and the benchmarks will show that. The benefit that's easy to overlook, though, is disk.
Every Rust checkout grows its own target/ directory. A few feature branches, a couple of PR reviews, a release branch, and the same dependency tree is compiled and stored on disk again and again.
This got sharper with AI-assisted development. Agentic workflows lean heavily on worktrees: an agent spins up a fresh checkout per task, and you can easily have a dozen of them alive at once. Each one is another full target/. The compile time is one cost; the duplicated gigabytes are another, and that one doesn't go away when the build finishes.
kache stores artifacts once in a content-addressed local store and restores them into each worktree by reference. So when you run ten worktrees, you're not paying for ten copies of the same .rlib. For branch-heavy and agent-heavy workflows, that's often the bigger win, and it's there before you measure a single millisecond.
C/C++ caching arrives
This one surprised us at first. kache is a Rust build cache, so why would people ask for C and C++ support?
Then it clicked. Plenty of Rust projects pull in C and C++ through cc-crate build scripts and -sys crates. Those native compiles are often the slowest, least-cached part of a clean build. If kache already wraps the compiler, extending it to the C family is a natural next step.
It's a big effort, so 0.3.0 ships initial support: single-source -c object compiles, the cc foo.c -o foo.o step that build scripts run thousands of times.
How the allow-list works
The interesting part is what kache refuses to cache, and why.
A C compiler has an enormous flag surface, and many flags change the bytes of the output object. If kache cached a compile while ignoring a flag it didn't understand, it could hand back the wrong artifact later. That's a silent miscache, the worst possible failure for a build cache: your build is wrong, and nothing tells you.
So C/C++ caching is built as an allow-list, not a deny-list. A compile is only cached if every argument falls into a category kache has explicitly classified:
- Modeled codegen flags: optimization and debug levels,
-fPIC,-std=, target arch. These go straight into the cache key. - Preprocessor-captured flags:
-D,-I,--sysroot,-includeand friends. Their entire effect already shows up in the preprocessed source, which kache hashes anyway. - No-effect flags: warnings, dependency-file generation (
-MD), and build mechanics. They don't change the object bytes of a successful compile.
Anything else (an unmodeled codegen flag, a cross-target option, a flag kache has simply never seen) forces a passthrough. The compile runs normally and isn't cached.
The trade is deliberate: an over-refusal costs you one ordinary compile; an under-refusal corrupts your build. We err hard toward "refuse."
The cache key itself is the fully preprocessed source (every transitively included header inlined) plus compiler identity and the modeled flags. Because the preprocessor expansion captures all the headers, any header change invalidates the key with no separate dependency tracking, and stripped line markers keep header paths out of the key so it stays portable across machines and worktrees.
Where the community comes in
Because the allow-list only grows as we classify flags, kache can tell you which flags in your build caused a passthrough. If a project isn't getting C/C++ hits, that report shows exactly what's missing.
This is a great place to contribute. Classifying a flag is a small, well-scoped, high-leverage change, and the people who know what a niche flag does are usually the ones already using it. We expect the allow-list to fill in much faster with community help than it would on our own.
A fallback wrapper for passthroughs
The allow-list isn't the only thing kache turns down. Link steps, MSVC, multi-source compiles, and anything else outside its modeled coverage all take the passthrough path too. Until now, every one of those passthroughs meant an uncached compile.
0.3.0 adds an optional safety net. Point kache at a fallback wrapper and it hands those passthroughs to a second compiler wrapper instead of straight to the bare compiler. The headline use case is sccache: kache caches what it can prove is safe, and sccache catches the rest. It's strictly opt-in (KACHE_FALLBACK, or [cache] fallback in the config file), it's never both wrappers on the same compile, and a fallback binary that's missing from PATH just warns once and degrades to a plain passthrough, so a misconfigured fallback can't fail a build.
That also changes the kache-versus-sccache story. They don't have to be an either/or choice anymore; in 0.3.0 they can run as a layered pair.
For the sources you'd rather keep away from kache entirely, there's a manual escape hatch. cache.exclude in .kache.toml takes glob patterns that bypass kache completely, with no lookup, store, or upload:
# .kache.toml
[cache]
exclude = [
"crates/problematic-crate/**",
"vendor/some-c-lib/**",
]
What about Windows?
Windows came up almost as often as C/C++. kache grew up on macOS and Linux, where its tricks (hardlinks, reflinks, filesystem sentinels) are well-trodden. Windows has different primitives and different sharp edges.
The good news: we've already started, and there's real groundwork in place. So far that includes:
- An exploratory Windows CI job, so the build and tests run on Windows as we go
- Cross-platform file locking: kache moved off the Unix-only
flockto a portable lock primitive - Cross-platform process and signal handling for the daemon, with the Unix-socket paths cleanly separated from a Windows code path
- A path normalizer that understands drive letters and normalizes Unicode, so cache keys stay portable
- A platform-abstraction layer that isolates the OS-specific pieces instead of scattering
cfgflags everywhere
This is groundwork, not a finished port, and we're not announcing a date yet. But Windows is no longer a "someday" item; it's something we're actively building toward. If it matters to you, telling us about your setup genuinely helps us prioritize the remaining work.
Heading to crates.io
A recurring piece of friction: installing kache. Today you install it via mise, cargo-binstall, or cargo install --git, and binstall in particular has tripped people up.
The cleaner answer is to be on crates.io, and we've got good news. The kache name on crates.io was already taken, but the previous owner has kindly agreed to transfer it to us. Once that completes, we'll start publishing releases there.
That means a plain cargo install kache, reliable cargo-binstall resolution, and the standard discovery path Rust developers expect. We'll announce it as soon as the transfer lands.
Restores got cheaper: reflink-first
The first post noted one caveat: hardlinks only work within a single filesystem. If your worktrees lived on a different volume than the kache store, restores fell back to copying.
kache now tries a reflink (copy-on-write clone) before reaching for a hardlink or a plain copy. On filesystems that support it (APFS, Btrfs, XFS), a reflink gives you an independent file that still shares physical blocks until one side is modified. It sidesteps some of the awkwardness of sharing a hardlinked inode across worktrees, and it makes same-volume restores cheap without the caveats.
The restore path is now: reflink if possible, hardlink if not, copy as a last resort. Most setups land on one of the first two.
Simpler setup with kache init
Early feedback made it clear that hand-editing ~/.cargo/config.toml and wiring up the background daemon was more ceremony than it should be.
kache init now handles first-run setup in one step: it configures the cargo wrapper, installs the daemon as a login service, and starts it. It's idempotent, so you can re-run it any time to repair a broken setup, and kache doctor verifies everything is wired correctly, including the migration path off sccache.
Thanks, and how to help
The response to kache has been better than we hoped, and the roadmap above is mostly your roadmap. C/C++ support and the crates.io push both came directly from people reaching out.
If you want to help shape what comes next:
- Classify C/C++ flags: the allow-list is the most contributor-friendly part of the codebase right now
- Tell us about Windows: real-world setups help us prioritize
- Send benchmarks and traces, especially surprising ones, good or bad
- File the edge cases: worktree layouts, exotic toolchains, CI quirks
GitHub: kunobi-ninja/kache GitHub Action: kunobi-ninja/kache-action
Cluster updates, in your inbox.
Kubernetes deep dives, GitOps field notes, and platform-engineering essays from the team building Kunobi. Two posts a month. No fluff.

