kache v0.4.1: what it takes to trust a build cache
A build cache that returns wrong answers is worse than no cache at all.
With no cache, a broken build is reproducible - run it again, get the same failure, fix it. With a cache that silently serves a stale artifact, you're debugging an environment problem that looks like a code problem. CI passes on one agent, fails on another. You strip out changes until the issue disappears, then it reappears two days later. The feedback loop is poisoned.
Speed is why you reach for a build cache. Correctness is why you keep using it. v0.4.0 and v0.4.1 are correctness and reliability releases: two cache key bugs fixed from real workloads, three hardening changes for CI environments, experimental C/C++ compiler caching, and - landing in v0.4.1 - first-class Windows support with a durability fix to back it up.
Cache keys need to hash the right things
The core job of a cache key is to fingerprint what matters. Hash too little and you get collisions: different builds mapped to the same key, wrong artifact served. Hash too much and you get false misses: two builds that would produce identical output treated as different, cache bypassed.
Machine-local noise falls into the second category. If the key includes something that varies per CI agent but doesn't affect output - a path, a hostname, a meaningless env var - you never get a cross-agent hit. You've built the infrastructure for sharing without actually sharing anything.
v0.4.0 ships two key version bumps, both from real bugs.
Key v7 removed -Clinker=<path> from the hash. The linker path is machine-local: /usr/bin/ld on one agent, /home/runner/.cargo/bin/rust-lld on another. Including it made cross-agent hits impossible for any build using a non-default linker. This surfaced during cross-clone key stability tests with kache-bench, a harness built around Firefox compile workloads: clone the same repo twice, build both, assert the keys match.
Key v8 normalized RUSTFLAGS before hashing. A trailing space produces a different hash from semantically identical flags - identical output, divergent keys. The fix is one line, but you have to know the bug exists. We know because real build systems set RUSTFLAGS in ways that include whitespace variation.
The broader principle: cache key correctness is a moving target. Every new flag, every new compiler version is a chance for machine-local state to leak into the hash. Purely analytical key design misses the cases you didn't think of. The bench-and-fix loop - run a real workload, surface a leak, fix it - is the only reliable model.
Reliability hardening
A cache that fails loudly is fine. A cache that fails silently is not. v0.4.0 adds three hardening changes.
Wrapper fallback on cache errors. A cache error - network timeout, disk full, corrupted index - no longer blocks the build. If kache can't complete a cache operation, it falls back to invoking the real compiler directly. The cache is an optimisation, not a hard dependency.
Atomic, durable entry registration. An OOM, a killed process, a signal at the wrong moment can leave an incomplete cache entry on disk. On the next build, kache finds the entry, treats it as valid, serves garbage. The fix: new entries land fully or not at all.
Remote artifact hash verification. When kache downloads an artifact from a remote cache, it verifies the content hash before use. This catches corruption in transit, corruption at rest, and anything more deliberate. Table-stakes for any remote cache used in security-sensitive CI.
None of these are glamorous. They're what makes a tool production-ready rather than demo-interesting.
C/C++ compiler caching, experimental
kache now intercepts cc invocations - clang and gcc - in addition to rustc.
This matters more than it looks. Many Rust projects carry substantial C and C++ in their dependency trees: -sys crates wrapping native libraries, build scripts invoking the system compiler, mixed-language codebases. By invocation count, a large Rust project can be majority C/C++ once you count everything cargo build touches. If kache only caches rustc, most of the work goes uncached.
The hard part is flag classification. C compilers accept hundreds of flags, and the set that affects output isn't the same as what appears in a typical invocation. Debug-info output paths, coverage instrumentation paths - these are machine-local and belong outside the key. Get this wrong in either direction and you get false misses or wrong artifacts.
The solution is a declarative flag classification table. Every flag is explicitly bucketed: key-relevant, key-excluded, or deferred. The table is auditable and extensible - a missing flag is a concrete, scoped fix. Gaps surface as false misses rather than wrong answers, which means they're safe to discover incrementally.
The table was validated against Firefox's build system - Gecko/Darwin baseline flags, WASM, ObjC, C++ ABI/RTTI/exception flags, clang wrapper flags - which gives a reasonable signal for real, complex builds. This release also introduces adapter descriptors replacing the old compiler-kind enum, and caches dep-info and .pp sidecar files alongside object files for incremental builds.
CC caching is local-only; remote is on the roadmap. The scope is deliberately constrained: single-source -c object compiles are cached. Link steps and multi-source units still pass through. Enough to be useful for a large class of builds, not yet the complete picture.
Windows: correctness on every platform
v0.4.1 makes Windows a first-class target.
The full end-to-end suite now runs on self-hosted Windows runners as a required CI check. A green pass means the cache round-trip - write, store, restore, verify - works on Windows. That's the honest definition of "supported."
Getting there required fixing a real correctness bug. The blob store was opening artifact files without write access before calling fsync. On Linux and macOS this fails silently in ways that don't break things. On Windows it fails in ways that do. The durability guarantees from v0.4.0 - entries land fully or not at all - weren't actually enforced on Windows. Blobs were written; the sync that makes them durable didn't happen.
The fix: open blobs writable when fsync is required. Platform-specific correctness bugs are easy to miss because the logic is identical and the tests pass - until you run them on the platform in question.
kache report: visible wins
kache has always deduplicated artifacts at rest. Two compilation units producing identical output share storage rather than duplicating it. The wins were real but invisible.
kache report changes that. Run it and you get a breakdown of storage saved by deduplication and restore method statistics: how cache hits are being served - local disk, remote, copy, hardlink.
"The cache is saving storage" is a hard claim to justify without numbers. "The cache saved 4.2 GB on this build" is not. Invisible wins are hard to defend when someone asks whether the cache is worth the complexity. Now they're not invisible.
v0.4.1 also adds the kache version to the report footer. When something goes wrong in CI, knowing which version was running is the first thing you need.
Try it
Update kache, run kache monitor against a real build, and check kache report to see what's actually happening.
If you're evaluating kache on a mixed Rust/C++ codebase, the experimental CC caching is worth enabling. When it misses on flags your build system passes that aren't in the classification table, file an issue - flag classification contributions are scoped and concrete: identify the flag, determine its bucket, verify the behaviour, send the PR.
Remote CC caching and broader C/C++ coverage are the next milestones. The correctness work across these two releases is the foundation that makes remote caching trustworthy enough to ship.
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.

