---
title: Architecture
description: The three pieces of kache — wrapper, store, and daemon — and how a build flows through them.
---

# 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
    ↓
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.

Build outcomes are separated into three cacheable cases:

| Outcome | Cache key | Compiler ran? | Blob content | Meaning |
|---|---|---:|---|---|
| `hit` | Found | No | Already known | kache restored from an existing entry |
| `dup` | Missed | Yes | Already known | a new key produced bytes kache already had |
| `miss` | Missed | Yes | New | a 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 S3, checking the remote 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](#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 has to ask S3 takes as long as the network 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](/docs/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 also 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 object-compile caching; that path is independent of the rustc wrapper. See [C/C++ caching](https://github.com/kunobi-ninja/kache#cc-caching-experimental).)

By default, kache skips only user-facing executables — `bin` crates and `--test` harness binaries — because their outputs depend on the linker and may be mutated post-build (code signing on macOS, stripping). dylib, cdylib, and proc-macro crates stay cached. Enabling `cache_executables` opts the executables in, but most of the compile-time savings come from caching rlibs anyway.

## 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 S3. 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](/docs/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, S3 sync, and metadata-driven prefetch keep working with no planner configured. (The hosted planner service itself is still in preview.)
