⚡ Hot tier — NVMe co-located with runner < 1ms
Action cache (AC) Action key → output digest map. Checked first on every lookup.
Content addressable storage Blob bytes keyed by digest. Read at NVMe speed when warm.
Cache Volume mount Persists across job teardown. Survives runner restart.
📦 Cold tier — Namespace artifact storage globally distributed
Infrequently accessed blobs Artifacts evicted from hot tier or not yet promoted.
Cross-workspace history Build outputs shared across branches and PRs.
Transparent promotion Frequently read blobs move to hot tier automatically.
1
Bazel computes action key
Hash of input digests + command + whitelisted env vars. Stable if the build is hermetic.
2
Hot tier AC lookup hit — <1ms
NVMe-backed Cache Volume returns the action result immediately. Steps 3 and 4 are skipped entirely for this action.
3
Cold tier AC lookup only on miss
Consulted only when the hot tier has no entry — first run after volume creation, or for actions that haven't run since the last cold start.
4
Blob read from hot tier CAS <1ms per file
Artifact bytes read from local NVMe. No network fetch. For a 10MB .rlib, this is ~0.1ms vs ~100ms over GCS.

On a warm cache, steps 2 and 4 handle 80–95% of actions entirely on local NVMe. The cold tier and remote network are only consulted for the delta: actions that genuinely changed since the last committed cache state.

Run 1 (cold)
empty volume build committed v1
Run 2 (warm)
committed v1 fork private copy exit 0 committed v2
PR A + PR B
committed v2 fork PR A copy exit 0 committed v3
committed v2 fork PR B copy exit 0 discarded (last-write-wins)
Failed job
committed v3 fork private copy exit 1 writes discarded committed v3
No concurrent corruption
Parallel PRs each get isolated forks. Writes never collide mid-run.
Failure safety
Exit non-zero = changes discarded. Partial builds never corrupt the committed state.
Last-write-wins
When two jobs both succeed, the later commit wins. Safe because Bazel outputs are deterministic: same inputs always produce identical outputs.
First run is always cold
No committed state exists yet. All actions fall through to the remote cache. Run 2 onwards is where the volume pays off.

The fork model mirrors how Git handles branches — each job gets its own working copy of the last stable state, and merges back only on clean completion. The difference is that Bazel's content-addressed outputs are deterministic, so merge conflicts are structurally impossible.

1
CI builds main branch, populates cache
Every action result and blob written by CI is committed to the shared workspace cache. Short-lived credentials scope access to the workspace, not to a specific machine.
2
Developer pulls branch, runs nsc cache bazel setup
The CLI writes a bazelrc pointing at the same Namespace cache endpoint CI used. Short-lived token is generated and expires after a few hours. No long-lived service account key needed.
3
Developer runs bazel build //...
Bazel queries the Namespace cache. Everything CI already built on the latest commit is a hit. No cold compile. The developer gets CI's cache for free.
4
Developer builds a new target locally
With --remote_upload_local_results=true in .bazelrc, the local build writes results back to the shared cache. CI and other developers can now hit those entries too.
No cold builds for developers
Pull a branch that CI already built. First local bazel build hits the cache.
Credential model
Short-lived tokens only. No GCS service account keys to rotate or leak.

Security note: Only enable --remote_upload_local_results=true for CI runs against merged code. For PR CI (untrusted code), use --noremote_upload_local_results to prevent unreviewed code from writing to the shared cache. See Julio Merino's cache poisoning writeup for the attack vector.

When committed cache content exceeds the configured volume size, the next run receives an empty volume: a silent cold start with no warning in CI logs. Size correctly from the start.

Codebase type
Rust monorepo
Go monorepo
JVM (Java / Kotlin / Scala)
Python
Rust monorepo
C++ monorepo
Mixed (Go + Rust)
Target count
5,500
Active engineers
10
VOLUME SIZE ESTIMATE
Estimated output footprint 158 GB 158 GB for 5,500 Rust targets + 10 engineers writing local results
Recommended volume size 221 GB 221 GB (1.4x buffer to avoid silent eviction)
0 GB cache usage at peak 221 GB configured
158 GB used
63 GB free

rlib files and debug symbols are large. Active monorepos exceed 50GB in days.

Under-sized
133 GB vol. fills in ~6w → exceeds limit cold start
Correct size
221 GB vol. stable for months → always warm

A 221 GB volume gives 63 GB of headroom above the estimated peak footprint. Monitor actual usage in the Namespace Usage Explorer and adjust if usage consistently exceeds 75% of configured size.