Bazel Remote Caching: When Shared Build Artifacts Actually Pay Off
Bazel remote caching lets teams share build artifacts through a content-addressed store. Here is how cache keys work, how to configure and verify it, and where it can backfire.
13 Sept 2025, 21:30 UTC

Your teammate just built the same 800 targets you are about to build. Your machine does not know that, so it recompiles everything from scratch. Remote caching is Bazel's answer to that waste: a shared, content-addressed store of action outputs so any client that has already done the work can hand the result to everyone else.
The thesis is simple: remote caching is one of the highest-leverage Bazel features to adopt, but only if your build is deterministic enough to hit the cache and your network is fast enough to make downloads cheaper than re-execution. Both conditions are testable before you commit.
How the cache decides what is "the same build"
Bazel models your build as a graph of actions: compile this file, link that binary, run this code generator. For each action, Bazel computes a digest over everything that could affect the output — the input file contents, the command line, environment variables, and the toolchain. That digest is the cache key. Outputs are stored in a content-addressable storage (CAS) service, keyed by the hash of their own bytes.
Two consequences follow. First, cache hits are exact: if anything meaningful changed, the key changes and the action re-runs. Second, anything untracked that affects output — say, a timestamp baked into a binary by a genrule reading the system clock — either poisons the cache or silently misses it. Determinism is not a nice-to-have here; it is the feature's foundation.
Turning it on
Bazel speaks the open Remote Execution API over gRPC, so the cache can be a commercial service, a cloud bucket-backed proxy, or a self-hosted open-source server such as bazel-remote or Buildbarn. The minimal configuration lives in your shared .bazelrc so every developer and CI runner picks it up:
# .bazelrc — applies to all builds
build --remote_cache=grpcs://cache.example.com:8980
build --remote_timeout=60
# Fail the build if the cache is unreachable? Usually no:
build --remote_local_fallback
Run this from your workspace root with normal build permissions; no special privileges are needed on the client. Authentication depends on the backend — commonly mTLS certificates or a header such as --remote_header=x-api-key=YOUR_KEY. Treat that key like any CI secret: keep it out of version control.
Two flags deserve attention. --remote_local_fallback tells Bazel to run the action locally when the remote call errors, which keeps a cache outage from becoming a build outage. And consider --remote_upload_local_results=false for read-only CI jobs that should never publish artifacts.
Verifying it works
Do not trust the flag; measure the behavior. Run a clean build twice and compare:
bazel clean
bazel build //... --remote_cache=grpcs://cache.example.com:8980
bazel clean
bazel build //... --remote_cache=grpcs://cache.example.com:8980
The second build's summary should show a high proportion of cache hits rather than locally executed processes — Bazel reports counts like "N processes: M remote cache hit" in its invocation summary. If the second build still compiles everything, something in your graph is non-deterministic or not cacheable. A practical next step is diffing the execution logs of two runs (--execution_log_binary_file) to find which actions changed keys between builds. Common culprits are embedded timestamps, absolute paths, and unstable ordering in generated files.
The honest trade-offs
Remote caching is not free. Every cache lookup and download crosses the network, and for very fast actions — a 50 ms compile — a 200 ms round trip is a net loss. Teams with the cache server in a different region sometimes see slower builds. Measure with your real latency before rolling out, and place the cache close to your CI fleet.
Security is the other real concern. A cache hit means you execute whatever bytes the server returns. If an attacker can write to your cache, they can ship you a backdoored artifact that every developer will happily link. Run the cache inside your trust boundary, require authentication and TLS, and restrict write access to trusted CI identities.
Finally, cache poisoning from non-determinism is insidious: a poisoned entry does not fail loudly, it just produces wrong-but-plausible artifacts. Stamp out timestamp embedding (use --stamp deliberately and Bazel's stable-status variables rather than ad hoc date calls) before you scale up sharing.
Where to start
Pick one CI pipeline, point it at a cache with --remote_local_fallback enabled, and compare clean-build wall time across a week against your baseline. If hits are high and builds are faster, extend the same .bazelrc flags to developers. If hits are low, you have learned something valuable: your build graph has nondeterminism worth fixing anyway — and fixing it benefits incremental builds, remote execution, and reproducibility far beyond caching.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.