Bazel Remote Caching: The Flag-Level Change That Can Cut Your Build Times
Bazel's remote cache skips re-executing work by keying action outputs on content digests. It's a flag-level change with big upside — if your build is hermetic enough to hit it.
20 May 2026, 18:49 UTC

Your CI pipeline rebuilds the same protobuf-generated code on every run, even though nothing it depends on has changed. Your teammate switches branches and waits twenty minutes for targets they built yesterday. This is the exact pain Bazel's remote cache is designed to remove — and unlike most build-speed projects, adopting it is a flag change, not a migration.
The thesis: remote caching is the lowest-risk, highest-leverage Bazel optimization most teams skip, but it only pays off if your build is hermetic enough to hit the cache reliably.
What the cache actually stores
Bazel models a build as a graph of actions — individual commands like compiling a file or linking a binary. Before running an action, Bazel computes a digest from everything that defines it: input file contents, the command line, environment, and tool versions. That digest is the cache key.
A remote cache is a content-addressed store mapping those digests to action outputs. When Bazel is about to execute an action, it asks the cache first. On a hit, it downloads the outputs and skips execution entirely. The win compounds across machines: a cache entry written by CI at 9 a.m. is a hit for every developer who syncs after standup.
Crucially, this is independent of remote execution. You don't need a fleet of remote workers. Caching alone runs everything locally and just shares results — a much smaller operational commitment.
Enabling it
Point Bazel at an HTTP(S) or gRPC endpoint that speaks the Remote Execution API's content-addressable storage (CAS) protocol — there are open-source and commercial implementations, and a simple nginx-backed HTTP cache works for smaller teams. Add to your .bazelrc (or pass on the command line):
# .bazelrc — verify flag names against your Bazel version
build --remote_cache=https://cache.example.com
build --remote_upload_local_results=trueNo build rule changes are required. Run bazel build //... twice from a clean output base and compare: the second run should show most actions skipped. To measure properly rather than eyeball it, enable the build event protocol (--build_event_json_file) or check your cache server's metrics, and look at the ratio of cache hits to total actions.
The failure mode nobody warns you about: low hit rates
A remote cache with a 15% hit rate is infrastructure you're paying for without the payoff. Hit rates collapse when action inputs are volatile — when the digest changes even though nothing meaningful did. Common culprits:
- Embedding a git commit SHA or build timestamp into a stamp file that every action depends on
- Tools that embed absolute paths or timestamps into their outputs
- Generated files that differ byte-for-byte between runs despite identical inputs
The fix is making inputs stable: gate stamping behind an explicit opt-in (Bazel's workspace status mechanism lets you separate stable from volatile status values), and pin toolchains so the compiler version doesn't drift between developer machines and CI.
There's a sharper hazard underneath: the cache assumes hermeticity. If an action secretly reads a file that isn't a declared input, the digest won't change when that file does — and the cache will happily serve a stale, wrong output. Non-deterministic actions that were harmless locally can actively poison a shared cache. Before rolling out broadly, run a clean build in CI against the populated cache and diff the outputs against a from-scratch build.
Controlling who writes
A pragmatic rollout pattern: let CI write, let developers read. Setting --remote_upload_local_results=false on developer machines makes the cache read-only for local builds. This shrinks the poisoning blast radius — a developer's odd local environment can't push bad entries — while still giving everyone the hits CI produced. Once you've measured hit rates and trust your hermeticity, you can open up writes more broadly.
The honest trade-off
Remote caching is not free. You're running (or paying for) a cache service, and cache effectiveness varies enormously by repository. A repo with heavy stamping and non-deterministic codegen may see negligible gains until that cleanup work happens — which is real engineering time. There's also a correctness risk that simply doesn't exist with purely local builds: a poisoned entry propagates wrong outputs to everyone until evicted. And flag names and defaults have shifted across Bazel versions, so check the flags against the version you actually run rather than copying config from a blog (this one included).
Where to start this week
- Stand up a test cache endpoint and enable
--remote_cachefor one CI job only. - Build twice from clean and measure the hit rate via the build event protocol.
- If hits are low, use
bazel aqueryor the execution log to find actions whose inputs change between runs. - Fix the top offenders, re-measure, then decide whether to roll out read access to developers.
Measure before you commit to operating a cache long-term. The teams that get burned are the ones who deploy the infrastructure first and discover the hit rate later.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.