Skip to content
How it works

Architecture

kache is made of three independent pieces that work together: the wrapper, the local store, and the daemon. Understanding what each one does makes it easier to configure kache correctly and diagnose problems when they appear.

The wrapper

When cargo builds a Rust project, it invokes rustc once per compilation unit. Setting RUSTC_WRAPPER=kache tells cargo to call kache instead, passing the real rustc path as the first argument.

kache detects this mode automatically: if its first argument looks like a path to rustc or clippy-driver, it runs as a wrapper. Otherwise it runs as the CLI.

For each rustc invocation, the wrapper does this:

parse args

compute blake3 cache key

check local store ──── hit → restore via reflink → done
    ↓ miss
check remote (via daemon) ── hit → restore via reflink → done
    ↓ miss
acquire per-key build lock

re-check local store ── peer committed → restore via reflink → done
    ↓ still absent
run rustc

store output files in content-addressed blobs

send upload job to daemon (async)

release lock

The lock prevents duplicate compilation when cargo spawns multiple parallel rustc processes for the same crate (which can happen in certain workspace configurations). A process that loses the lock waits for the winner's result and restores from the cache instead of compiling. A process that acquires the lock re-checks the store before compiling, covering a peer that committed between the initial lookup and lock acquisition.

Build outcomes are separated into three cacheable cases:

OutcomeCache keyCompiler ran?Blob contentMeaning
hitFoundNoAlready knownkache restored from an existing entry
dupMissedYesAlready knowna new key produced bytes kache already had
missMissedYesNewa new key produced at least one new blob

hit is shown as one row, but kache records three hit kinds in the event log — local_hit, prefetch_hit, and remote_hit — distinguished by where the entry was found. Invocations that bypass the cache (non-primary, excluded, or a skipped executable) record a fourth outcome, passthrough.

dup is a cache-key/content outcome, not the same thing as the storage deduplication metric. A high dup count can point to over-specific cache keys or noisy inputs, because different keys are compiling to identical bytes.

Non-primary invocations — rustc calls with no source file, like dependency probing — pass straight through without touching the cache.

The local store

The store lives under your platform cache dir — ~/Library/Caches/kache on macOS, ~/.cache/kache on Linux ($XDG_CACHE_HOME/kache), %LOCALAPPDATA%\kache on Windows — and has two parts:

SQLite index (index.db) tracks every cache entry: crate name, cache key, file list, feature flags, target, and profile. It runs in WAL mode with a 5-second busy timeout so 300+ parallel rustc processes can all hit it without contention.

Content-addressed blobs (store/blobs/) hold the actual compiled files, sharded into 256 subdirectories by the first two hex chars of the hash. Each blob is named after its blake3 hash, so two crates that happen to produce an identical artifact share the same physical file. When kache restores a cache hit it reflinks the blob into the build's output dir on copy-on-write filesystems (APFS, btrfs, XFS-with-reflink): zero-copy, but with an independent inode so a later write never mutates the cache blob.

On filesystems without reflink, kache falls back to a hardlink for immutable artifacts (.rlib / .rmeta) or a plain copy for files that may be mutated post-build (executables, dylibs, proc-macros).

The daemon

The daemon is a long-running background process that handles everything async: uploading new artifacts to the configured remote, checking it before a build starts, and prefetching artifacts for upcoming crates.

The wrapper communicates with the daemon over a Unix socket (daemon.sock inside the cache dir — see The local store for the platform path; a named pipe on Windows) using lightweight RPC calls. Local-only calls — queuing an upload, a cached remote-check answer — return in well under a millisecond; a remote check that reaches the configured backend takes as long as that round-trip. If the daemon is down, the wrapper continues without it — local caching works normally, remote features degrade gracefully.

The daemon runs as a separate binary invocation (kache daemon run) and can be managed as a system service via launchd (macOS) or systemd (Linux). See Daemon lifecycle for details.

What kache does not touch

As a RUSTC_WRAPPER, kache intercepts only rustc and clippy-driver invocations — not cargo, ld, or any other tool; linking, proc-macro expansion, and build scripts run unmodified. Separately, kache can be set as CC / CXX to wrap a C/C++ compiler — gcc/clang, or clang in MSVC driver mode (clang-cl / --driver-mode=cl, as used by mozconfigs and the cc crate on Windows) — for local object-compile caching. That path is independent of the rustc wrapper. See C/C++ caching.

Executables — bin crates and --test harness binaries — are cached by default on Linux and skipped by default on macOS and Windows, where debug info is referenced from outside the binary (see #319 and cache_executables). dylib, cdylib, and proc-macro crates stay cached everywhere. Restored executables are independent copies rather than hardlinks, so a post-build strip or code-signing step cannot write back into a store blob. The final binary is often the single most expensive unit on a warm build's critical path, so caching it is worth materially more than its one-unit share suggests.

The remote planner

By default, prefetch is driven entirely by the client running cargo metadata and asking the daemon to pull every crate in the resolved graph from the configured remote. This is good but not optimal — the metadata pass is workspace-local and can't reason about which artifacts are most likely to hit on the current toolchain / target / feature set.

A separate planner service (see Remote service) accepts manifests from clients (kache save-manifest), stores them in an embedded database, and replies to prefetch queries with a ranked candidate list. The client path is already wired: when KACHE_PLANNER_ENDPOINT is set, the daemon asks the planner before each build. When the planner has no useful data it returns a fallback disposition and the client behaves exactly as it does without it. The planner is purely additive — local caching, remote sync, and metadata-driven prefetch keep working with no planner configured. (The hosted planner service itself is still in preview.)

Available for:
Apple macOS logomacOSMicrosoft Windows logoWindowsLinux logoLinux
Download Kunobi