zccache Implementation Roadmap
September 1, 2026 · View on GitHub
This document describes the phased implementation plan for zccache, a high-performance local compiler cache daemon written in Rust. Each phase builds on the previous one and is designed to be independently shippable and testable. An engineer should be able to pick up any phase and start implementing once its predecessors are complete.
Phase 0: Scaffolding and Standards
Goals: Establish project infrastructure. Every crate exists (even if empty), CI is green, coding standards are enforced, and foundational types are in place.
Non-goals: No runtime functionality yet. Nothing listens on a socket, nothing compiles code, nothing caches artifacts.
Deliverables:
- Rust workspace (
Cargo.tomlat root) with all crates stubbed out:zccache-core-- shared types, error types, tracing setup, path abstractionszccache-hash-- cache key computation (blake3)zccache-protocol-- IPC message definitions and serializationzccache-fscache-- file metadata cachezccache-artifact-- content-addressed artifact storezccache-watcher-- file-system watcher abstractionzccache-compiler-- compiler argument parsing and wrappingzccache-ipc-- platform-specific IPC transport (Unix domain socket / named pipe)zccache-daemon-- daemon binaryzccache-cli-- CLI binaryzccache-test-support-- shared test utilities, fixtures, temp-dir helpers
- CI pipeline (GitHub Actions):
cargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcargo test --workspace- MSRV check (1.95.0)
- Matrix: Linux (ubuntu-latest), macOS (macos-latest), Windows (windows-latest)
.rustfmt.tomlwith project conventions- Clippy configuration (
clippy.tomlor workspace-levelCargo.tomllint settings) cargo-denysetup: license checks, advisory database audit, duplicate crate detectionARCHITECTURE.md,DESIGN_DECISIONS.md,ROADMAP.md(this file)- Core error types using
thiserrorinzccache-core - Tracing setup (
tracing+tracing-subscriber) with structured logging, env-filter, and a default subscriber initializer inzccache-core - Cross-platform path abstraction types in
zccache-corefor normalizing and comparing paths consistently across Linux, macOS, and Windows
Key tests:
- Workspace compiles on all three platforms (Linux, macOS, Windows)
- CI pipeline passes end-to-end
- Core error types can be constructed and formatted
- Tracing subscriber initializes without panic
Risks: None significant. This phase is pure infrastructure.
Phase 1: Minimal Daemon + CLI + IPC
Goals: CLI can start the daemon, send a ping, and get a response. This phase establishes the foundation for all future communication between CLI and daemon.
Non-goals: No caching, no compilation, no file watching, no artifact storage.
Deliverables:
- Daemon binary (
zccache-daemon) that listens on:- Unix domain socket (Linux / macOS)
- Named pipe (Windows)
- CLI binary (
zccache-cli) that:- Discovers a running daemon (checks well-known socket/pipe path)
- Auto-starts the daemon if none is running
- Sends a ping request and prints the pong response
- IPC transport abstraction in
zccache-ipc:Transporttrait withconnect,send,recvmethodsUnixTransportimplementation (tokioUnixStream)NamedPipeTransportimplementation (tokio named pipe on Windows)
- Protocol message types in
zccache-protocol:Ping/PongShutdownStatus(daemon uptime, version, connection count)- Length-prefixed prost framing
- Daemon lifecycle management:
- Startup: bind socket/pipe, write lock file (containing PID)
- Lock file: prevent multiple daemon instances
- Stale socket/lock detection: if lock file PID is dead, clean up and take over
- Idle timeout: shut down after configurable period with no connections
- Signal handling: graceful shutdown on SIGTERM/SIGINT (Unix) and Ctrl-C (Windows)
Key tests:
- Daemon starts and accepts connections on all platforms
- CLI auto-starts daemon when no daemon is running
- Ping/pong round-trip completes successfully
- Multiple CLI instances can connect concurrently
- Stale socket/lock file is detected and cleaned up
- Daemon shuts down gracefully on signal
- Daemon shuts down after idle timeout
- Cross-platform IPC works (verified by CI matrix)
Risks: Platform-specific IPC bugs, especially Windows named pipes with tokio (less mature than Unix domain sockets). Mitigation: Test on all three platforms in CI from day one; keep the transport trait narrow so platform-specific code is isolated and replaceable.
Phase 2: Local Artifact Caching
Goals: Store and retrieve compilation artifacts by cache key. No real compiler integration yet -- use synthetic test data to exercise the full store/lookup path.
Non-goals: No real compiler wrapping, no file watching, no metadata cache.
Deliverables:
- blake3-based cache key computation in
zccache-hash:- Deterministic key builder that accepts ordered inputs (compiler identity, flags, source hash)
- Key type: 32-byte blake3 digest, hex-encoded for filesystem paths
- Content-addressed artifact store in
zccache-artifact:write(key, artifact_data) -> Result<()>: store artifact bytes under keyread(key) -> Result<Option<ArtifactData>>: retrieve artifact bytes by keyexists(key) -> bool: check for cached artifact- Storage layout:
<cache_root>/objects/<first-2-hex>/<remaining-hex>/ - Atomic writes: write to temp dir under
<cache_root>/tmp/, then rename into place - Manifest file per artifact: output file(s), stdout, stderr, exit code, timestamp, size, checksum
- Index (originally planned as
redb; shipped as an in-memory map snapshotted toindex.bin— see DD-008) for:- Cache key to artifact path mapping
- Access time tracking (for LRU eviction)
- Total cache size tracking
- LRU eviction:
- Dynamic 5% cache budget (40-200 GiB) with byte/percent overrides
- Soft pressure at 85% evicts entries older than 4 days to 70% of budget
- Hard pressure at 100% or low free space evicts LRU entries regardless of age to 80% of budget and the recovery free-space target
- Daily full maintenance expires entries older than 30 days
- Corruption detection:
- Manifest includes blake3 checksum of artifact data
- On read, verify checksum; if mismatch, delete entry and return miss
- Protocol messages in
zccache-protocol:Store { key, artifact }/StoreResponse { success }Lookup { key }/LookupResponse { hit, artifact? }CacheStats/CacheStatsResponse { total_size, entry_count, hit_count, miss_count }
- Daemon integration: handle
Store,Lookup,CacheStatsrequests - CLI commands:
zccache stats: display cache statisticszccache clear: purge all cached artifacts
Key tests:
- Store an artifact and retrieve it correctly
- Atomic write prevents partial/corrupt artifacts on crash (simulate with process kill)
- Concurrent stores of the same artifact key are safe (no corruption, last-write-wins or dedup)
- Eviction removes oldest entries when cache exceeds threshold
- Corrupt artifacts (tampered data) are detected and removed on read
zccache statsreports accurate numberszccache clearremoves all entries and resets stats- Cache survives daemon restart (index persisted to disk)
- Cache key is deterministic: same inputs always produce same key
Risks: redb edge cases under highly concurrent access (many simultaneous writes to the same table). Mitigation: Run stress tests with 50+ concurrent writers; wrap redb access behind a service with controlled concurrency; keep the redb schema simple (two tables: keys, access times).
Note
Retained as written for the historical record. This risk was real but was resolved by removing the database rather than by taming it: the index became an in-memory DashMap whose only writer is the daemon's background flush (DD-008, superseded). The "wrap access behind a service with controlled concurrency" mitigation is essentially what shipped.
Phase 3: File Metadata Cache
Goals: Maintain a fast in-memory cache of file stat metadata to reduce redundant filesystem stat calls during cache key computation. This is a building block for Phase 4 (watcher) and Phase 5 (compiler wrapper).
Non-goals: No file watcher yet (metadata is updated only by explicit stat calls). No content hashing integration (digest is stored opportunistically but not computed automatically).
Deliverables:
- File metadata types in
zccache-fscache:- `FileMetadata { path, mtime, size, file_id, content_digest: Optionblake3::Hash, confidence, last_verified }
file_id: inode number (Unix) or file index (Windows) for detecting file replacement
DashMap-based metadata cache:lookup(path) -> Option<FileMetadata>: return cached metadata if confidence >= thresholdstat_and_update(path) -> FileMetadata: stat the file, update cache, return fresh metadatainvalidate(path): remove entry from cacheinvalidate_prefix(dir): remove all entries under a directory
- Stat verification logic:
- Compare
(mtime, size, file_id)from cache against actual stat result - If all three match: entry is still valid, bump confidence to
High - If any differ: entry is stale, replace with fresh stat, clear
content_digest
- Compare
- Confidence levels:
High: recently verified by explicit stat (within last N seconds)Medium: watcher reports no change (not used yet, reserved for Phase 4)Low: stale, needs re-verification before use
- Confidence decay: entries degrade from
HightoLowover time (configurable decay interval, default 30 seconds) without re-verification - Opportunistic content digest storage: when a caller computes a file's blake3 digest, it can store it alongside the metadata for future lookups
- Cache introspection: entry count, hit/miss counters, average confidence distribution
Key tests:
- Metadata lookup returns correct
mtime,size,file_idvalues - Changed file (different mtime or size) is detected on re-stat
- Replaced file (same path, different inode/file_id) is detected
- Confidence levels degrade from
HightoLowover time invalidate(path)removes the entryinvalidate_prefix(dir)removes all entries under that directory- Concurrent access from multiple threads is safe and correct
- Large caches (100k+ entries) maintain acceptable lookup latency (< 1 microsecond p99)
- Opportunistic digest is cleared when file metadata changes
Risks: Subtle race conditions between calling stat and the actual file state changing (TOCTOU). Mitigation: The metadata cache is never the sole source of truth for correctness -- always re-stat before trusting a cached digest for cache key computation. The metadata cache is a performance optimization, not a correctness mechanism.
Phase 4: File Watcher Integration
Goals: A file watcher updates the metadata cache in near-real-time, reducing the need for explicit stat calls during builds. The watcher boosts metadata confidence but is never the sole source of truth.
Non-goals: The watcher is not relied upon for correctness. No header dependency tracking. No recursive deep-watch of entire filesystem.
Deliverables:
- Watcher abstraction trait in
zccache-watcher:Watchertrait:watch(path),unwatch(path),recv() -> WatchEventWatchEvent { path, kind: Created | Modified | Removed }
notify-based implementation:- Linux: inotify
- macOS: FSEvents
- Windows: ReadDirectoryChangesW
- Polling fallback mode:
- Used when native watcher is unavailable or unreliable
- Configurable poll interval (default: 2 seconds)
- Detects changes via stat comparison against cached metadata
- Watcher-to-metadata-cache integration:
- On
Modified/Createdevent: set entry confidence toMedium, clear cachedcontent_digest - On
Removedevent: invalidate entry - "No news is good news": if watcher is healthy and reports no event for a path, confidence stays at current level (does not decay as fast)
- On
- Overflow handling:
- If the OS event queue overflows (e.g., Linux inotify queue full, Windows buffer overflow): downgrade all watched entries to
Lowconfidence - Log a warning with
tracing - Re-establish watches
- If the OS event queue overflows (e.g., Linux inotify queue full, Windows buffer overflow): downgrade all watched entries to
- Watch scope management:
add_watch(directory): start watching a directory (non-recursive by default, recursive opt-in)remove_watch(directory): stop watching- Daemon tracks which directories are relevant based on compilation requests
- Watcher lifecycle:
- Watcher runs on a dedicated OS thread (not tokio runtime) to avoid blocking async tasks
- Events forwarded to daemon via
tokio::sync::mpscchannel - Watcher restarts automatically after transient errors
Key tests:
- File modification is detected by watcher within reasonable time (< 1 second on native, < poll interval on polling)
- Metadata cache confidence is updated to
Mediumon watcher event - Content digest is cleared on modification event
- File removal triggers cache invalidation
- Overflow event causes all watched entries to downgrade to
Low - Polling fallback detects changes when native watcher is unavailable
- Watcher restarts after transient error
- Adding and removing watch scopes works correctly
- Cross-platform watcher works (verified by CI matrix)
- Watcher handles rapid successive changes to the same file (debouncing or last-event-wins)
Risks: Platform-specific watcher quirks -- macOS FSEvents coalesces events and may deliver them late; Windows ReadDirectoryChangesW can silently overflow its buffer; Linux inotify has a per-user watch limit. Mitigation: Polling fallback is always available; the defensive confidence model means watcher bugs cause at worst a performance regression (extra stat calls), not correctness bugs.
Phase 5: Compiler Wrapper MVP
Goals: Actually cache gcc/clang C compilations end-to-end. A user can run zccache wrap -- gcc -c foo.c -o foo.o and get transparent caching.
Non-goals: C++ support (complex template/header interactions), header dependency tracking beyond what the preprocessor provides, MSVC (cl.exe) support.
Deliverables:
- Compiler argument parser in
zccache-compiler:- Detect compiler type from argv[0] or explicit flag:
gcc,cc,clang - Extract key fields: source file, output file (
-o), include paths (-I), defines (-D), optimization level, warning flags, standard (-std=), target (-target,-march) - Classify flags as: affects-output (part of cache key), does-not-affect-output (ignored), unknown (makes invocation non-cacheable or pass-through)
- Detect compiler type from argv[0] or explicit flag:
- Cacheability check -- reject and pass through:
- Linking (no
-cflag, or-shared,-o a.out) - Preprocessing-only (
-E,-M,-MM) - Multiple source files in one invocation
- Assembly output (
-S) - Reading from stdin (
-) - Invocations with unknown/unrecognized flags (conservative: pass through rather than risk incorrect caching)
- Linking (no
- Cache key computation:
- Compiler identity: blake3 hash of compiler binary (by path), plus
compiler --versionoutput - Sorted relevant compiler flags (normalized)
- Relevant environment variables (
CPATH,C_INCLUDE_PATH,SDKROOT, etc.) - Preprocessed source hash: blake3 of
compiler -E <source>output (captures all included headers)
- Compiler identity: blake3 hash of compiler binary (by path), plus
- Preprocessor integration:
- Run
compiler -E -fdirectives-only <args> <source>to get preprocessed output - Hash the preprocessed output as the source component of the cache key
- Use metadata cache (Phase 3) to skip re-preprocessing when source and includes haven't changed (future optimization)
- Run
- Wrapper entrypoint:
zccache wrap -- gcc -c foo.c -o foo.o- Parses args, checks cacheability, computes cache key, queries daemon
- On cache hit:
- Copy cached artifact (object file) to output path
- Replay cached stdout to stdout
- Replay cached stderr to stderr
- Return cached exit code
- On cache miss:
- Execute the real compiler with original arguments
- Capture stdout, stderr, exit code
- If exit code == 0: store artifact (object file + stdout + stderr + exit code) in cache via daemon
- If exit code != 0: do not cache, pass through output as-is
- Debug mode (
zccache wrap --debug -- gcc ...):- Print cacheability decision and reason
- Print cache key inputs and final key
- Print hit/miss result and timing breakdown
- Protocol messages in
zccache-protocol:CompileRequest { cache_key, compiler_args, source_path, output_path }CompileResponse { hit, artifact?, exit_code, stdout, stderr, timing }
Key tests:
- Simple C file (
hello.c) compiles and the result is cached - Second compilation of the same file is a cache hit with identical output
- Changing source file content causes a cache miss
- Changing a compiler flag (e.g.,
-O0to-O2) causes a cache miss - Changing an included header causes a cache miss (preprocessor output changes)
- Non-cacheable invocations (linking,
-E, multiple sources) pass through to the real compiler without error - Concurrent compilations of different files work correctly
- Concurrent compilations of the same file work correctly (no corruption)
- Exit code is propagated correctly (both 0 and non-zero)
- Stderr from the compiler is replayed on cache hit
- Debug mode prints useful, accurate information
- Wrapper works when the daemon is not running (auto-starts it)
Risks: Compiler argument parsing is complex and varies between compiler versions. Unrecognized flags could lead to incorrect caching if not handled conservatively. Mitigation: Start with a small, curated set of recognized flags; treat all unrecognized flags as making the invocation non-cacheable (pass through). This is safe: it can only cause cache misses, never incorrect cache hits. Expand the recognized flag set incrementally based on real-world usage.
Phase 5.5: Build System Configure Cache (zccache meson configure)
Goal: Skip the meson setup (configure) phase on identical inputs. The compile cache (Phase 5) already handles the per-TU step; this addresses the once-per-build-cycle configure cost that the compile cache cannot reach.
Motivation: Issue #627. Field measurement on a large FastLED checkout: the configure phase alone is 50 s on a cold run (683 build targets, 1434 ninja rules). The same checkout on a warm cache restores in 8 s — a 6.2× speedup, 42 s saved per build cycle. Multiply by N rebuilds per day × M developers.
Deliverable (shipped): zccache meson configure --source-dir SRC --build-dir BUILD [-- meson-args...]. Implemented in crates/zccache/src/cli/commands/meson_cache.rs.
Cache key (blake3 with domain tag "zccache-meson-cache-v1"):
[meson.build, meson.options, meson_options.txt]discovered recursively under source dir, each (relative-path, content) pair sortedmeson --versionoutput- Source-dir + build-dir absolute paths (same-build-dir restriction)
- Selected env vars (
CC,CXX,CFLAGS,CXXFLAGS,LDFLAGS,PKG_CONFIG_PATHalways; extras via--input-env) - Trailing
meson_argsverbatim
Storage: flat hand-rolled archive ([u32 path_len][path][u64 content_len][content] repeated) at ~/.cache/zccache/meson-configure/<key>/build-dir.tar plus cached stdout/stderr sidecars.
Same-build-dir restriction. Build-dir-portable caching would require rewriting absolute paths meson scatters through meson-info/ and meson-private/ — see issue #627 open question 2. The current scope solves the common dev-loop case (stable build dir per developer) and the CI matrix case (each tuple converges to a per-tuple cache entry on its second invocation per platform).
Failure modes:
- Meson exit code != 0 → do NOT cache. A re-run after the fix gives meson a fresh chance.
- Cache restore I/O error → log warning, fall through to a fresh meson setup. Self-healing.
Tests: crates/zccache/tests/cli/cli_meson_configure_cache.rs — TDD-pinned MISS → HIT → invalidate-on-meson.build-change.
Future work (not in v1):
- CMake / Bazel equivalents (same shape, different file set)
--reconfigureinteraction — currently we always serve a cache hit if the key matches; meson's own--reconfiguredecision happens after our hit check by definition. If a user relies on meson's reconfigure heuristics outside the meson.build content (e.g. environment-only changes), they should add those vars to--input-env.- Build-dir-portable mode via on-restore path rewriting
Phase 6: Correctness Hardening and Benchmarks
Goals: Ensure the system is correct under stress, measure performance, and make the system observable and debuggable. This phase turns the MVP into production-quality software.
Non-goals: Exotic optimizations, distributed caching, or support for additional compilers.
Deliverables:
- Stress tests:
- 100+ concurrent compilations with random file mutations
- Rapid file changes during compilation (verify no stale artifacts returned)
- Daemon restart during active compilations (verify graceful recovery)
- Cache eviction under pressure (verify no data loss or corruption)
- Property-based tests (
proptestorquickcheck):- Cache key determinism: same inputs always produce same key, regardless of insertion order
- Metadata cache correctness: for any sequence of file operations and cache operations, the cache never returns stale data that leads to incorrect cache hits
- Argument parser round-trip: parsed args reconstruct to equivalent command line
- Benchmarks (
criterion):- Cache key computation latency (target: < 100 microseconds for typical invocation)
- Metadata cache lookup latency (target: < 1 microsecond p99)
- Artifact store write/read latency (target: < 5 ms for 1 MB artifact)
- IPC round-trip latency (target: < 500 microseconds for ping/pong)
- End-to-end cache-hit compilation (target: < 10 ms overhead vs. direct compiler invocation)
- Observability improvements:
tracingspans on all critical paths: IPC handling, cache lookup, artifact read/write, compiler execution, preprocessor execution- Cache hit/miss reasons in structured log fields
- Timing breakdown in logs: total time, preprocessing time, hashing time, IPC time, artifact copy time
- Daemon status endpoint: active connections, cache stats, watcher health
- CLI diagnostic commands:
zccache inspect <key>: show what is stored for a given cache key (manifest, artifact size, creation time, last access time)zccache explain -- gcc -c foo.c -o foo.o: dry-run that shows cacheability decision, computed cache key, and whether it would be a hit or miss, without actually compilingzccache health: check daemon status, cache integrity sample, watcher status
- Documentation:
- Crate-level
//!docs for every crate ///docs for all public types and functions- Usage guide: installation, configuration, basic usage, troubleshooting
- Performance tuning guide: cache size, watcher configuration, confidence intervals
- Crate-level
Key tests:
- All stress tests pass on all platforms
- All property tests pass with at least 10,000 cases
- All benchmarks run without regression vs. established baselines
zccache inspectreturns accurate datazccache explainoutput matches actual compilation behaviorzccache healthdetects and reports known-broken states
Risks: Performance regressions as new features are added. Mitigation: Run criterion benchmarks in CI; track results over time; set regression thresholds that fail the build if exceeded (e.g., > 10% regression on any benchmark).
Phase 7 (Future): Advanced Features
Goals: Post-MVP improvements that extend zccache to cover more use cases and improve performance. These are not committed to specific timelines.
Non-goals: Not committed to implementing all items. Each item will be evaluated independently based on user demand and engineering capacity.
Possible deliverables:
- C++ support: Handle C++ compilation, including template-heavy code and precompiled headers (PCH). Requires expanded argument parser and careful cache key computation for PCH dependencies.
- MSVC (
cl.exe) support: Windows-native compiler with a very different flag syntax (/c,/Fo,/I,/D). Requires a separate argument parser module. - Header dependency tracking: Use
-MD/-MMDor compiler-generated.ddependency files to track which headers a source file includes. Enables smarter cache invalidation without running the full preprocessor on every build. - Artifact compression: Compress cached artifacts with zstd before writing to disk. Reduces cache storage footprint by 50-70% for typical object files. Decompress on read.
- Remote artifact storage: Push/pull artifacts to/from remote backends (S3, GCS, Azure Blob). Enables cache sharing across machines.
- Distributed cache sharing: Multiple developers or CI machines share a cache. Requires content trust model, access control, and network-aware eviction.
- Persistent metadata cache: Serialize the file metadata cache to disk on daemon shutdown; reload on startup. Reduces cold-start stat overhead after daemon restart.
- IDE integration: Provide a language-server-compatible interface or editor plugin that shows cache status, hit rates, and diagnostics inline.
- Build system plugins: Native integration with CMake (custom compiler launcher), Meson (compiler wrapper config), and Bazel (remote cache protocol).
- Configuration hot-reload: Reload
zccache.tomlconfiguration without restarting the daemon. Useful for changing cache size limits, watch scopes, or log levels at runtime. - Metrics export: Expose Prometheus-compatible metrics endpoint from the daemon for monitoring cache hit rates, latency distributions, cache size, and watcher health in production environments.
Key tests: Defined per feature as they are picked up.
Risks: Scope creep; each feature should be evaluated for complexity vs. user value before committing. Mitigation: Treat each item as an independent mini-project with its own design doc, deliverables, and acceptance criteria before implementation begins.