Part 1 of a three-part series. Part 2 covers caching, remote execution, and the software supply chain. Part 3 covers real-world adoption against Gradle and Buck2. This article stands alone.
Bazel matters because it turns a build from a pile of scripts into a build graph, and once the graph is explicit, it can be analyzed, cached, parallelized, sandboxed, and executed on another compute node.
Every engineering organization eventually hits the same wall. The codebase grows faster than the system that compiles it, and two problems arrive together: builds get slow, and build scripts get uncertain. The slowness is visible on every engineer’s clock. The uncertainty is quieter and more corrosive, since almost nobody is sure the incremental build actually works, so the team falls back on the one thing it trusts, the slow clean build from scratch, to avoid voodoos. This was my personal experience even in well-established forks of U-Boot, the Linux Kernel, and Android.
That reflex is not superstition. It is a rational response to a build system that cannot describe itself. A Makefile carries no true, enforceable record of what each command actually reads and writes. When the real inputs and outputs of a step are implicit, the tool cannot prove that a cached or skipped result is still valid, so the only guaranteed-correct build is a serial rebuild of everything. Speed and correctness are the same missing capability seen from two sides.
Bazel exists to close that gap, and understanding why is the best possible introduction to whether it deserves a place on your roadmap. The thesis of this article is simple: Bazel matters because it turns a build from a pile of scripts into a build graph, and once the graph is explicit, it can be analyzed, cached, parallelized, sandboxed, and executed on another compute node. Everything else is details in service of that one move.
By the mid-2000s, Google’s monorepo had outgrown GNU Make’s implicit, filesystem-driven model. In 2006, Google began developing Blaze to introduce an explicit model of packages, targets, rules, actions, and artifacts. This structure allowed the system to reason about dependencies, cache keys, invalidation, parallelism, and remote execution much more reliably than a timestamp-driven graph could.
By explicitly declaring inputs and outputs rather than inferring them, Blaze made builds knowable, enabling safe parallelism, reliable output reuse, and independence from the developer’s workstation. This achieves speed through correctness by proving when work is redundant. Blaze eventually incorporated multi-language rules, distributed execution, and a Java-rewritten engine supporting multi-threaded, stateful, incremental builds.
Google released Bazel in March 2015 as an open-source, refined port of Blaze, naming it with an anagram of the original tool. This open-sourcing process was far from a straightforward copy-and-paste effort. It required transforming an internal utility, which had safely assumed Google’s unique infrastructure, source layout, and machinery, into a versatile system capable of surviving on arbitrary repositories, varying workstations, and three different operating systems.
That portability work is why Bazel looks the way it does today. The system ships as a native C++ launcher that finds your workspace, chooses output locations, and parses configuration. It carries a long-lived Java server that holds the build graph. It exposes Starlark, a Python-like language, so that build rules can be written and versioned outside Bazel’s core rather than patched into it. The client and server speak a stable protocol across Linux, macOS, and Windows. Bazel reached 1.0 in October 2019, which also established the long-term support model enterprises rely on.
Calling Bazel simply “a fast build tool” fails to capture its true essence. In reality, Bazel operates as a core contract linking four distinct parties: (a) the developers crafting the BUILD files, (b) the rules converting those files into actions, (c) the tools executed by those actions, and (d) the system managing their execution. True speed is achieved only when these rules explicitly declare the complete truth regarding inputs, outputs, configurations, toolchains, and execution strategies. When this truth is fully articulated, the engine can safely cache, parallelize, and distribute workloads with absolute confidence. Conversely, leaving details ambiguous forces the system to be conservative, because ambiguity risks producing incorrect outputs rather than just slower builds. This fundamental framework is essential for understanding Bazel’s history, architectural design, and failure modes.
Bazel’s release history represents a succession of explicit, enforced contracts. Every iterative wave transforms what was once implicit or ad hoc into a declared, verifiable component of the build graph.
| Release | Year | The contract it tightened |
|---|---|---|
| 1.0 | 2019 | Stability itself: semantic versioning and support windows, so teams could plan migrations |
| 4.0 | 2021 | An enterprise operating model: the first LTS track alongside a rolling track |
| 5.0/6.0 | 2022 | Dependency resolution: Bzlmod and MODULE.bazel emerge, then become real, replacing ad hoc WORKSPACE fetching |
| 7.0 | 2023 | Remote-first by default: Bzlmod, Build without the Bytes, and Skymeld all turned on |
| 8.0 | 2024 | Modularity: legacy WORKSPACE off by default, built-in rules leaving the core, symbolic macros |
| 9.0 | 2026 | Finality: WORKSPACE removed entirely, external modules mandatory and explicit |
With a constant trajectory, every development step moves toward greater explicitness, thereby unlocking superior cacheability, distribution, and governance, even as it demands more from the platform team managing the shift. This ongoing tightening is evident across three key structural transitions: dependencies migrate from basic scripts into formal supply-chain policies; rules decouple from a centralized Java monolith into a versioned Starlark ecosystem; and configurations transform from disconnected flags into a typed framework of platforms and toolchains. Reflecting this continuous refinement, the ecosystem as of mid-2026 has version 9.1.1 as its current stable release, alongside active rolling 10.0 pre-releases and sustained cleanup initiatives.
Bazel’s speed on warm builds comes from a deliberate split between a short-lived client and a long-lived server. When you run bazel,
a small native C++ client takes control. It finds the boundary of your workspace, extracts the embedded Java server the first time it is needed, and reads your configuration files. Then it looks for a server already running for this workspace. If one exists with matching startup options, the client connects to it. If not, it starts a fresh Java server.
That server is central to Bazel’s design. Bazel’s command-line client connects to a long-lived server process, and that server keeps BUILD files, Skyframe nodes, dependency-graph state, analysis results, and other metadata warm across invocations. As a result, a second build can often avoid much of the loading and analysis work paid by the first build, unless the server has restarted or the relevant parts of the graph were invalidated. The client and server communicate through Bazel’s command-server protocol, defined with Protobuf and transported over RPC. In the normal one-output-base model, a Bazel server handles one command at a time; running concurrent Bazel commands for the same workspace generally requires separate output bases, which means separate server instances.
bazel run is a revealing special case. The server builds your binary but cannot launch it, because the server has no terminal attached and the client owns the terminal. So the server tells the client which program to execute and with what arguments, and the client replaces itself with your program as a direct child of your shell. Per-workspace state is isolated on disk under an output base, which is why a given output base is typically served by a single running server. This separation, one lightweight client per invocation and one durable server per output base, is the mechanical reason a warm Bazel feels instantaneous while a cold one does not.
Once a command is accepted, every build moves through the same three phases. The analogy to TensorFlow is imperfect but helpful: TensorFlow turns model code into a dataflow graph of tensor operations; Bazel turns build declarations into a dependency and action graph of software artifacts. In both cases, the graph is what lets the system reason globally instead of just running commands one by one. The cleanest way to hold the phases in mind is as a pipeline that turns text into a plan and then into files.
| Phase | Input | What happens | Output |
|---|---|---|---|
| Loading | MODULE.bazel and BUILD files | Each BUILD file is compiled and run as a Starlark program, resolving the .bzl files it loads | In-memory Package objects |
| Analysis | Packages plus command-line flags | Flags become build configurations; each target is paired with a configuration into a configured target; rule logic runs and registers actions | A bipartite action graph of action nodes and artifact nodes |
| Execution | The action graph | Inputs are staged into the execution root; each action's cache is consulted; misses run in a hermetic sandbox locally or are executed on another compute node (remote execution — the subject of Part 2) | Artifacts written into the execution root |
Loading answers “what exists.” Analysis answers “what would we do and how.” Execution answers “do only what is not already done.” The action graph produced by analysis is bipartite by design: action nodes, such as compile, link, and archive commands, are connected to artifact nodes, the files, by input and output edges. That structure is what makes the build a graph you can reason about rather than a script you can only run.
Running an action in a sandbox that exposes only its declared inputs is what Bazel means by a hermetic build: the action cannot silently read anything the graph does not know about, so its result depends only on inputs Bazel can name and hash. Hermeticity is the precondition for everything Part 2 is about, because a result is only safe to cache or reuse on another machine if the same declared inputs are guaranteed to produce the same output.
Underlying all three phases is Skyframe, an evaluation engine that models the build as a graph of keys mapped to values computed by functions. Crucially, functions discover dependencies dynamically during execution rather than declaring them upfront.
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.
Between builds, Skyframe compares the filesystem against the previous build’s state and marks affected nodes dirty. A dirty node first checks its own dependencies. If none of them actually changed, it is marked clean and skipped. If they did change, it is rebuilt.
The optimization that makes warm builds feel free is change pruning. When a dirty node is re-evaluated and produces a value identical to last time, the change stops there. Its version stamp is unchanged, and its parents are never invalidated. Edit only a comment in a source file, and while the file itself looks different, the action that consumes it may hash to the same result, so nothing downstream re-runs. Skyframe tracks, per node, the version when its value last changed versus when it was last evaluated, and a parent can skip re-evaluation whenever no child has changed more recently than the parent was last evaluated. This is why a no-op bazel build on a warm server returns in milliseconds. Nothing is rebuilt, because the graph can prove nothing needs to be.
Bazel’s twenty-year history centers on one concept: builds are graphs. By strictly defining their edges, these graphs can be cached, parallelized, distributed, and governed. Every key architectural decision serves this end: the declarative model, the resident server, the three phases, Skyframe’s restarts, and change pruning all ensure the build graph is explicit and trustworthy.
This philosophy is central to Incredibuild, founded in 2002 (four years before Google started Blaze in 2006) on the identical premise that a build is work waiting to be distributed across existing cores. Distributed compilation developed in parallel across multiple places, a shared lineage explored in Part 2.
Organizations evaluating Bazel usually start by mapping their existing build graph: inputs, outputs, generated files, toolchains, environment assumptions, and which actions can safely be cached, skipped, or distributed. That work is valuable even if the final answer is not Bazel, because it exposes the real shape of the build and the hidden assumptions that make it slow or fragile.
This is also where Incredibuild offers a different path. For many teams, Incredibuild can accelerate existing builds without requiring a full rewrite of Makefiles, project files, or the build system itself. It is often the pragmatic, plug-and-play route: keep the build system you already have, add distributed execution and caching around it, and get speed without forcing a migration first.
For teams that do choose the high-control path of a custom Bazel environment, Incredibuild can still help. With a bring-your-own-build-system approach, it can support the migration journey and provide an additional execution layer for scaling work across local machines, CI fleets, and cloud capacity. The goal is simple: whether you adopt Bazel, stay with your current build system, or run a hybrid model, compute should not be the bottleneck.
Yossi Eliaz
Incredibuild empowers your teams to be productive and focus on innovating.