Part 2 of a three-part series. Part 1 covered Bazel’s history, architecture, and Skyframe’s incremental engine. Part 3 will cover adoption strategy against Gradle and Buck2, and where Incredibuild fits.
For teams relying on BwoB (Build without the Bytes) and remote execution, the remote cache becomes much closer to load-bearing infrastructure. If an intermediate output exists only as a CAS digest, the blob must remain available long enough for the rest of the build to use it.
CI pipelines spend much of their time repeating work: compiling the same files, relinking the same binaries, and producing the same artifacts across machines. Bazel’s advantage is not just speed. It is the ability to decide, with evidence, when work can be reused.
That decision turns the build system into supply-chain infrastructure. Remote caches, the CAS, Bazel Central Registry, lockfiles, and mirrors are no longer side details. For teams that rely on remote execution and Build without the Bytes, they become part of the production platform.
Bazel does not simply cache “a compile.” It caches a precisely described action. Locally, Bazel records action-cache entries that compare the current action and artifact metadata against prior execution state. Remotely, REv2 identifies an action by the digest of a canonical Action protobuf. That Action points to a Command digest and an input-root Merkle-tree digest, and includes fields such as timeout, salt, cacheability, and, since REv2.2, platform properties. The platform is also declared in the Command.
The Command contains the argv, environment variables, working directory, and declared output paths. The input root describes the declared input files by path and content digest. When those values match, Bazel can ask whether an equivalent result is available. The identity alone does not skip execution; the cache hit does.
This is also why paths matter. Touching a source file without changing its bytes may cause Bazel to re-stat or re-digest it, but it should not invalidate downstream actions if the effective digest and declared path are unchanged. Moving the same bytes to another declared path is different: paths are part of the input tree, so the action identity can change.
Skyframe’s in-memory evaluation cache lives in the long-running Bazel server and answers, “must Bazel re-evaluate this graph node?” The local action cache lives under the output base and answers, “must this command run again on this machine?” The disk or remote cache answers, “has anyone already produced an equivalent result?”
Only the third layer turns a build into a distributed system. It lets one developer or CI job reuse work produced elsewhere, provided the action identity, outputs, and cache policy line up.
The effect is easy to measure. In a typical build, a cold start against an empty remote cache takes significantly longer than a subsequent run from a fresh environment. With remote caching enabled, a substantial majority of executable actions are served directly from the cache, eliminating the need for re-execution.
With remote caching alone, a miss means Bazel executes the action locally and may upload the result for later reuse. With remote execution enabled, the execution service fetches the required inputs from the CAS, runs the command on a compatible worker, uploads outputs back to the CAS, and returns result metadata to the action cache.
Inputs are uploaded as a Merkle tree of directories and file digests. Shared subtrees, such as toolchains and external repositories, can be reused without re-uploading the same blobs. On a cache miss, a remote worker fetches the required inputs from the CAS, runs the command, uploads outputs back to the CAS, and returns result metadata to the action cache.
This protocol is what makes remote execution scalable. Bazel does not send an SSH session to a particular machine. It hands an execution service a content-addressed workload that any compatible worker can run.
Build without the Bytes became the default for builds using remote caching and/or remote execution in Bazel 7: –remote_download_outputs defaults to toplevel, so Bazel no longer downloads intermediate outputs of remote actions into the local output base by default.
That saves bandwidth, especially in large builds where intermediate object files dwarf the final binary. Bazel records remote outputs in its output tree as metadata and fetches bytes lazily when a later local action, test, run command, or user request needs them. In the measured runs above, both stages ran under –remote_download_toplevel; most of the residual 15.7 seconds on the warm run is analysis plus fetching top-level outputs, not execution.
The trade-off is operational. For teams relying on BwoB (Build without the Bytes) and remote execution, the remote cache becomes much closer to load-bearing infrastructure. If an intermediate output exists only as a CAS digest, the blob must remain available long enough for the rest of the build to use it.
Remote caches have finite storage, so CAS blobs can disappear. Bazel 7 improved the operational story for that failure mode: a dedicated eviction exit code 39, retry support via –experimental_remote_cache_eviction_retries, cache TTL settings via –experimental_remote_cache_ttl, and lease extension to keep referenced blobs alive during long builds.
In supported cases, Bazel can rewind the graph and re-execute the upstream action for a missing blob instead of failing immediately. This major improvement is not a guarantee; correct recovery still depends on the executor, cache policy, action reproducibility, and execution strategy.
External dependencies are the other half of the story. WORKSPACE could fetch repositories, but it did not provide a structured transitive version model. Bzlmod adds MODULE.bazel, versioned modules, registries, module extensions, overrides, lockfiles, and deterministic Minimal Version Selection.
The timeline matters. Bzlmod became the default external dependency system in Bazel 7. Bazel 8 disabled the legacy WORKSPACE system by default. Bazel 9 removed the code supporting WORKSPACE entirely.
That moves dependency management from build-script convenience into platform policy. Teams now need clear rules for registries, mirrors, lockfiles, allowed overrides, and whether builds may touch the public internet.
In January 2023, GitHub changed its source archive generation mechanism. SHA-256 checksums changed for many generated source archives even though the underlying contents did not. Bazel correctly rejected the mismatches, which broke builds but also showed why checksum pinning matters.
On December 26, 2025, expired certificates for many *.bazel.build domains caused widespread Bazel build failures. The postmortem was published in January 2026, but the outage itself happened in December. The lesson is not to weaken verification. The lesson is to own mirrors, lockfiles, download caches, and dependency availability as production concerns.
When a function requests an uncomputed dependency, Skyframe returns null, causing the function to also return null. Skyframe then resolves the missing dependency and restarts the parent function from the top. This restart mechanism allows Bazel to evaluate massive, self-discovering graphs in parallel without advance knowledge of their shape, ensuring correctness, since functions only finish once all requested dependencies exist.
Bazel turns build work into explicit, content-addressed actions. That is why remote caching, remote execution, and BwoB can be safe and fast. It is also why remote cache, CAS, registries, lockfiles, and mirrors become operational responsibilities.
For platform teams, the winning model is disciplined and pragmatic: declare the graph precisely, verify dependencies strictly, operate the cache as production, and use acceleration where it pays back. That is how build correctness becomes build speed.
Someone has to run this infrastructure, and it isn’t free. Part 3 covers the vendor landscape and operating models, and how adoption plays out against Gradle and Buck2.
Yossi Eliaz
Incredibuild empowers your teams to be productive and focus on innovating.