Cache key
The cache key is a blake3 hash that uniquely identifies one compiled artifact. Two rustc invocations that would produce identical output must produce the same key. Two invocations that differ in any input that affects the output must produce different keys.
Getting this right is the core correctness challenge in any build cache. A false positive (same key, different output) corrupts the build. A false negative (different keys, identical output) wastes time.
Inputs to the key
The key is computed from these inputs, in order:
Key version. A hardcoded integer (CACHE_KEY_VERSION) that's bumped whenever the key computation logic changes. This invalidates all existing entries without requiring a manual purge.
rustc version. The full output of rustc --version --verbose, which includes the commit hash and host triple. A toolchain update produces different keys for everything. To avoid running rustc --version hundreds of times in a parallel build, kache caches the output to disk keyed by the binary's mtime — the first wrapper process writes it, the rest read it in under a millisecond.
Target triple. The --target flag, or the host triple if none was specified.
Crate identity. Crate name, crate types (lib, rlib, proc-macro, etc.), and edition.
Codegen options. All -C key=value flags, sorted by key for determinism. The incremental option is excluded — it's a path that varies by machine and doesn't affect the compiled output.
Features and cfg flags. Feature flags sorted alphabetically, then all other --cfg flags sorted. Order-independence is important here: --cfg feature="a" --cfg feature="b" and --cfg feature="b" --cfg feature="a" must produce the same key.
Source files. This is the most important input. Rather than hashing just the crate root (src/lib.rs), kache runs a dep-info pre-pass — it invokes rustc --emit=dep-info before the real compilation, which produces a Makefile-style dependency file listing every .rs file the crate depends on, including modules, include!() targets, and build script outputs. All of those files are hashed with blake3.
The dep-info pass also captures env!() and option_env!() dependencies — environment variables that are baked into the binary at compile time. Their values go into the key too.
Extern dependencies. For each --extern name=path argument, kache hashes the referenced rlib/rmeta. This means a change to a dependency automatically invalidates all crates that depend on it, propagating correctly through the dependency graph.
Sysroot crates (std, core, alloc) can't be read from their installed path on other machines, so kache uses a sentinel instead of their path hash. Their identity is already captured via the rustc version.
RUSTFLAGS and CARGO_ENCODED_RUSTFLAGS. Both are included. Paths inside these flags are normalized by replacing the current working directory with ., so flags like -L /home/runner/project/target/release/deps produce the same key as -L /home/alice/project/target/release/deps.
CARGO_CFG_ variables.* These carry platform detection results (e.g., CARGO_CFG_TARGET_OS=linux). They're sorted by name before hashing.
Linker identity. For binary and dylib outputs, the linker's --version output is included. This prevents serving a binary built with one linker to a machine with a different one. Like the rustc version, this is cached to disk keyed by the linker binary's mtime.
Remap status. kache adds --remap-path-prefix to normalize source paths in debug info, making artifacts portable. But when coverage instrumentation is active (-C instrument-coverage), path remapping is skipped so that coverage tools can map profiling data back to source files. The key reflects this decision.
Key salt (optional). If KACHE_KEY_SALT / cache.key_salt is set, its value is folded into the key. This is an escape hatch for toolchain divergence the key cannot otherwise observe — e.g. a glibc/mold/linker bump or a Nix store rebuild that changes compiled output without changing any tool's --version banner. Unset (the default) has no effect. See Configuration → Cache-key salt.
Extra cache-key inputs (optional). A crate can declare files the compiler reads at compile time but never reports — sqlx's .sqlx/query-*.json, migrations/, include!'d data — in a co-located kache.toml. Each matched file is folded as its crate-relative path plus content hash, so editing one (or swapping two files' contents, where the filename→content binding is load-bearing, like migration order) re-keys that crate, while a worktree move does not. The declared glob set is folded too, so editing kache.toml re-keys even at zero matches. Opt-in, per-crate, and union-only — a misdeclared glob can only cost an extra miss, never restore a wrong artifact. See Configuration → Extra cache-key inputs.
Extra cc allowlist flags (optional). For C/C++ compiles, any flag listed in cc.extra_allowlist_flags / KACHE_CC_EXTRA_ALLOWLIST_FLAGS that the built-in allow-list doesn't model is folded into the key verbatim when present on the command line, so a different flag or value always produces a different key. Unset (the default) has no effect. See Configuration → Extra cc allowlist flags.
Cross-machine portability
The key is designed to be the same on any machine that has the same:
- Rust toolchain version
- Target triple
- Source code
- Feature flags and cfg values
- RUSTFLAGS (after path normalization)
This is what makes the remote cache useful in CI: a developer's laptop and a fresh CI runner with the same toolchain produce identical keys, so CI can restore what the developer already built.
Debugging cached binaries
The same --remap-path-prefix rules that make artifacts portable also rewrite the paths baked into debug info. A cached binary's DWARF (or PDB on Windows) refers to your sources through stable sentinels instead of the machine-local path they were compiled at:
| Sentinel | Real location |
|---|---|
<WORKSPACE> | the repo / workspace checkout |
<CARGO_HOME> | usually ~/.cargo (registry + git deps) |
<RUSTUP_HOME> | usually ~/.rustup (std sources) |
<HOME>, <TMPDIR>, <TARGET> | per-user / per-job directories |
So a backtrace or a debugger opened against a restored binary will show source paths like <WORKSPACE>/src/main.rs and won't find the file on disk until you map the sentinel back. This is expected — it is the price of one binary being valid in every checkout — not a sign the cache is corrupt.
This page describes the rustc artifact cache. C/C++ object compiles (cc / clang-cl, see C/C++ caching) are keyed separately — and clang-cl debug compiles (-Z7 / -g) aren't cached yet (they pass through), so there's no clang-cl PDB to remap here.
Point your debugger at the real checkout with a source map. For your own crate sources, map <WORKSPACE>:
# in ~/.gdbinit or at the prompt
set substitute-path <WORKSPACE> /abs/path/to/your/checkout
# in ~/.lldbinit or at the prompt
settings set target.source-map <WORKSPACE> /abs/path/to/your/checkout
Add additional substitute-path / source-map entries for <CARGO_HOME> or <RUSTUP_HOME> if you want to step into dependency or std sources.
Coverage builds are exempt. When coverage instrumentation is active (-C instrument-coverage), kache skips path remapping entirely (see Remap status above), so tools like cargo-llvm-cov and tarpaulin map profiling data back to real source paths with no extra configuration.
Debugging cache misses
If a crate keeps missing when you expect a hit, use kache why-miss <crate-name>. It compares the cache key from the last known entry against the current build and reports which input changed.
You can also set KACHE_LOG=kache=trace to see every component being hashed during key computation.