# Bazel, From Zero

> Google's build system for huge, multi-language repos: hermetic, reproducible builds with aggressive caching and parallelism - and the steep tradeoff that buys.


---

# Bazel, From Zero

You opened a repo, ran the build, and got coffee. Forty minutes later it was still going - and your one-line change rebuilt the entire world. Bazel exists to make that not happen: it knows exactly what each piece depends on, rebuilds only what actually changed, and runs everything it can in parallel. The price is that you have to tell it the truth about your code, in its own language, up front.

This guide is about that bargain. What Bazel is really doing under the hood, how you live with it day to day, and the real question of whether your project is big enough to need it at all.

## How to read this

Go in order. Phase 1 builds the mental model - the dependency graph and why hermeticity is the whole point. Phase 2 is the everyday loop: writing BUILD files, naming targets, running builds and tests. Phase 3 is the reality check - remote caching, the slow cold start, and the projects where Bazel is the wrong tool. If you only have ten minutes, read Phase 1; it's the part that makes everything else click.

## The phases

1. [Phase 1: The Graph, and Why Hermetic](01-the-graph-and-why-hermetic.md) - what Bazel actually models and why declared inputs change everything.
2. [Phase 2: BUILD Files and the Daily Loop](02-build-files-and-the-daily-loop.md) - targets, rules, and the commands you'll run a hundred times a day.
3. [Phase 3: Caching, Cold Starts, and When Not To](03-caching-cold-starts-and-when-not-to.md) - remote cache, the real costs, and choosing the right tool.


---

# The Graph, and Why Hermetic

Most build tools you've met run a script. Make runs recipes, npm runs lifecycle hooks, a shell script runs commands top to bottom. They do what you tell them, in the order you tell them, and they trust you to know what changed. That trust is exactly where slow, flaky builds come from: the tool can't rebuild only what changed because it doesn't actually know what depends on what.

Bazel starts from the opposite end. Before it runs anything, it builds a picture of your whole project as a graph - every file, every output, every dependency between them. Then it figures out the smallest set of work needed to give you what you asked for. The build commands are almost an afterthought; the graph is the product.

## A build is a graph, not a script

Picture a small service: some library code, a binary that uses it, and a test for the library.

```text
//lib:greet  ──►  //app:server  (binary depends on the library)
     │
     └──────────►  //lib:greet_test  (test depends on the library)
```

*What just happened:* Each box is a **target** - a named thing Bazel can build. The arrows are dependencies. If you change `//lib:greet`, Bazel knows that both `//app:server` and `//lib:greet_test` are downstream and need rebuilding. If you change a file that only `//app:server` reads, the test is untouched and Bazel won't run it.

This is the core mental model: **you don't run builds, you ask for nodes in a graph, and Bazel computes the rest.** Everything else in Bazel - caching, parallelism, the strict rules about inputs - falls out of this one idea. Once the graph exists, Bazel can rebuild any node by rebuilding only its changed ancestors, and run independent nodes at the same time because it knows they can't affect each other.

## Hermetic: declare your inputs or it doesn't count

Here's the part that feels strict at first and turns out to be the whole point.

A normal build action can read anything on your machine - a header in `/usr/include`, an env var, the system clock, a tool that happens to be on your `PATH`. That's why "works on my machine" exists: the build secretly depended on something it never declared, and that something was different on the next machine.

Bazel runs each build action in a sandbox that contains **only the inputs you declared** - nothing else from your filesystem. If your compile step needs a header, that header has to be a declared dependency, or the action literally can't see the file and fails. This is what **hermetic** means: the build's result depends only on its declared inputs, not on hidden state.

```text
declared inputs  ─►  [ sandboxed action ]  ─►  declared outputs
   (sources,                  ▲
    deps, tools)              │
                    nothing else is visible
```

*What just happened:* Because the action can only see what it declared, the same inputs always produce the same outputs - on your laptop, on CI, on a teammate's machine three time zones away. That reproducibility is what makes the cache trustworthy: if the inputs match a previous build, the output is guaranteed identical, so Bazel can hand you the cached result instead of doing the work again.

> Hermeticity is annoying right up until the moment it saves you. The first time you fight Bazel because it "can't find" a tool that's clearly installed, remember: it's not broken, it's refusing to let an undeclared dependency rot your build six months from now.

## Why this scales when scripts don't

Tie the two ideas together. The graph tells Bazel the minimal work; hermeticity makes every result cacheable and shareable. Put those together across a thousand-engineer monorepo and you get the thing Bazel was built for:

- A change to one library rebuilds that library and its dependents - **not the whole repo**.
- Two unrelated targets build **in parallel** because the graph proves they don't interact.
- A result your colleague already built is in a **shared cache**, so you download it instead of compiling it (more on this in Phase 3).
- Tests whose inputs didn't change are **skipped** - Bazel already knows they'd pass identically.

A shell script can't do any of this safely, because it doesn't know the graph and can't trust that an action only touched what it declared. Bazel can, because you paid for that knowledge up front.

## For builders

The same idea shows up in other modern tools - content-addressed caching, dependency graphs, sandboxed actions. If you've used a tool that hashes inputs to skip work, you've met a slice of this. The general principle of describing builds as artifacts and stages lives in [build and release basics](/guides/build-and-release-basics); Bazel is one rigorous, large-scale answer to those same questions.

```quiz
[
  {
    "q": "What is Bazel primarily computing before it runs any build command?",
    "choices": [
      "The fastest shell script to execute top to bottom",
      "A dependency graph of targets, to find the minimal work needed",
      "A list of every file that changed since the last commit",
      "The optimal number of CPU cores to reserve"
    ],
    "answer": 1,
    "explain": "Bazel models the project as a graph of targets and their dependencies, then derives the minimal set of actions to produce what you asked for."
  },
  {
    "q": "What does it mean for a Bazel build action to be hermetic?",
    "choices": [
      "It runs entirely in memory with no disk writes",
      "It is encrypted so other users cannot read the output",
      "Its result depends only on its declared inputs, not hidden system state",
      "It always runs on a remote machine instead of locally"
    ],
    "answer": 2,
    "explain": "A hermetic action sees only its declared inputs in a sandbox, so the same inputs always produce the same outputs - which is what makes caching trustworthy."
  },
  {
    "q": "Why can Bazel safely skip rebuilding a target whose inputs are unchanged?",
    "choices": [
      "It assumes most code rarely changes",
      "Because hermetic actions guarantee identical inputs produce identical outputs",
      "It checks the file modification timestamp and trusts it",
      "It re-runs the build but discards the result quietly"
    ],
    "answer": 1,
    "explain": "Hermeticity means identical declared inputs yield identical outputs, so a cached result is provably the same as rebuilding - no need to redo the work."
  }
]
```


---

# BUILD Files and the Daily Loop

Phase 1 was the why. This is the part your hands actually do: writing the files that describe the graph, naming the things in it, and running the handful of commands you'll type all day. The good news is that the daily surface area of Bazel is small. Most of your time is spent in two files and three commands.

## Where the graph comes from: BUILD files

Bazel doesn't infer your graph by reading source code. You write it down, in a `BUILD` (or `BUILD.bazel`) file that lives in each directory you want to be a **package**. Inside, you declare targets using **rules** - `cc_binary`, `java_library`, `py_test`, and so on - in a Python-like language called Starlark.

Here's a real package for a small Python app:

```python
# app/BUILD.bazel
py_library(
    name = "greet",
    srcs = ["greet.py"],
)

py_binary(
    name = "server",
    srcs = ["server.py"],
    deps = [":greet"],          # depends on the library above
)

py_test(
    name = "greet_test",
    srcs = ["greet_test.py"],
    deps = [":greet"],
)
```

*What just happened:* You declared three targets in one package. `name` is how you'll refer to each one. `srcs` lists the source files this target owns. `deps` lists the other targets it needs - `:greet` means "the target named greet in this same package." Bazel reads this and now knows the graph: `server` and `greet_test` both point at `greet`. Notice you never wrote a single compile command. The rule (`py_binary`) knows how to build a Python binary; you only supply the pieces.

This is the trade Phase 1 described, made concrete. You spend effort declaring inputs and deps precisely. In return, Bazel can do correct incremental builds, caching, and parallelism - because you told it the truth.

## How to name a target: labels

Every target has a globally unique address called a **label**. You'll read and type these constantly, so the syntax is worth ten seconds:

```text
//app:server
 │    │
 │    └─ target name (from the `name =` in the BUILD file)
 └────── package path from the repo root (the // means "workspace root")
```

*What just happened:* `//app:server` means "the target named `server` in the package at `app/`." The `//` always anchors to the root of your workspace, so the same label means the same thing from anywhere in the repo. Inside the same BUILD file you can shorten it to `:server`. A common shorthand `//app` (no colon) means `//app:app` - the target whose name matches its directory.

## The three commands you'll actually use

Ninety percent of daily Bazel is three verbs.

```console
$ bazel build //app:server
INFO: Analyzed target //app:server (3 packages loaded, 12 targets configured).
INFO: Found 1 target...
Target //app:server up-to-date:
  bazel-bin/app/server
INFO: Elapsed time: 2.1s, Critical Path: 1.8s
INFO: Build completed successfully, 4 total actions
```

*What just happened:* `bazel build` produced the binary and put it under `bazel-bin/`. Run it again with no code changes and it finishes in milliseconds with `0 total actions` - nothing changed, so there was nothing to do. That's the incremental graph working.

```console
$ bazel test //app:greet_test
INFO: Analyzed target //app:greet_test (0 packages loaded, 0 targets configured).
//app:greet_test    PASSED in 0.4s

Executed 1 out of 1 test: 1 test passes.
```

*What just happened:* `bazel test` built the test's dependencies, ran it in a sandbox, and reported the result. Run it again without touching anything and you'll see `(cached) PASSED` - Bazel knows the inputs are identical, so the outcome is guaranteed identical and it skips the run.

```console
$ bazel run //app:server
INFO: Build completed successfully, 1 total action
Serving on http://localhost:8080
```

*What just happened:* `bazel run` builds the target and then executes it, in one step. Use `build` when you only want the artifact, `run` when you want it built and launched.

## Wildcards and querying the graph

You rarely name targets one at a time. Wildcards let you act on whole slices of the graph:

```console
$ bazel test //app/...        # every test under app/, recursively
$ bazel build //...           # build everything in the workspace
```

*What just happened:* `...` means "this package and all packages beneath it." `bazel test //...` is the classic "did I break anything anywhere" command - and because of caching, after the first run it only re-tests what your change actually touched.

When you need to understand the graph itself, `bazel query` answers questions the BUILD files can't at a glance:

```console
$ bazel query "deps(//app:server)"          # everything :server depends on
$ bazel query "rdeps(//..., //app:greet)"   # everything that depends on :greet
```

*What just happened:* `rdeps` (reverse deps) is the one you'll reach for before a risky change: "if I touch `//app:greet`, what else could I break?" The graph that powers the build also answers that question directly.

> A daily habit that pays off: when a build fails with a missing-symbol or missing-file error, your first instinct should be "I forgot a `deps` entry," not "the compiler is broken." Hermeticity means the file is invisible until you declare it. The fix is almost always adding the right label to `deps` or `srcs`.

## For builders

The `WORKSPACE` / `MODULE.bazel` file at your repo root is where external dependencies get declared - pinned versions, fetched and cached the same hermetic way as everything else. You won't touch it daily, but know it's the seam between your code and the outside world. Adding a third-party library means declaring it there once, then referencing its label from your `deps`, never reaching out to a global package install.

```quiz
[
  {
    "q": "In the label //app:server, what does the 'app' part refer to?",
    "choices": [
      "The name of the rule, like py_binary",
      "The package: the directory (from the workspace root) containing the BUILD file",
      "The name of the output binary file on disk",
      "The Bazel command being run"
    ],
    "answer": 1,
    "explain": "A label is //package:target. 'app' is the package path from the workspace root; 'server' is the target's name inside that package's BUILD file."
  },
  {
    "q": "You run `bazel test //app:greet_test` twice with no changes in between. What does the second run do?",
    "choices": [
      "Re-runs the test fully to be safe",
      "Reports it as cached and skips re-running, since inputs are identical",
      "Fails because the result already exists",
      "Rebuilds every target in the workspace first"
    ],
    "answer": 1,
    "explain": "Identical declared inputs guarantee an identical result, so Bazel reports a cached PASSED rather than executing the test again."
  },
  {
    "q": "A build fails because a source file 'cannot be found,' yet the file clearly exists in the directory. What is the most likely cause?",
    "choices": [
      "The compiler is misconfigured",
      "Bazel needs a reboot to refresh its file index",
      "The file is not declared in srcs or deps, so the sandboxed action can't see it",
      "The label uses // instead of a relative path"
    ],
    "answer": 2,
    "explain": "Hermetic actions only see declared inputs. An undeclared file is invisible inside the sandbox; the fix is adding it to srcs or the right deps entry."
  }
]
```


---

# Caching, Cold Starts, and When Not To

You've got the model and the daily loop. Now the part nobody tells you until you're three months in: where Bazel's promised speed actually comes from, where it bites back, and the most important engineering call of all - whether your project should be using Bazel at all. The plain answer is that for a lot of projects, it shouldn't.

## The remote cache: building once for the whole team

Phase 1 said hermeticity makes results shareable. This is where that pays off. Because a hermetic action's output is fully determined by its inputs, Bazel can name each action by a hash of its inputs and store the result in a **remote cache** that the whole team and CI share.

```text
your change ─► Bazel hashes each action's inputs
                       │
            ┌──────────┴───────────┐
       hash hit                hash miss
            │                       │
   download result          run action locally,
   from remote cache        upload result for others
```

*What just happened:* When you build a target a colleague (or CI) already built from the same inputs, Bazel computes the same hash, finds it in the shared cache, and **downloads the output instead of compiling it.** On a big repo this is the difference between a fresh checkout taking an hour and taking two minutes. This is also why teams adopt Bazel: not for the local loop, but for the shared one.

The bigger sibling is **remote execution** - Bazel ships the sandboxed actions themselves to a farm of build machines and runs hundreds in parallel, far beyond your laptop's core count. Both features are optional and need a backend (self-hosted or a vendor). Plain Bazel without them still caches locally and parallelizes across your own cores.

> Remote caching only works if your builds are genuinely hermetic. One action that secretly reads the system clock or an undeclared file will poison the cache - someone downloads a "matching" result that's actually wrong. The strictness Bazel forces on you in Phase 1 is the entry fee for this feature. There's no cheating it.

## The costs nobody puts on the brochure

Bazel is not free, and the bill comes due in specific places. Name them so they don't ambush you.

- **The cold start is brutal.** A clean build with an empty cache builds the world from scratch. Bazel's speed is incremental and shared; the very first build on a fresh machine with no remote cache can be slower than the simple tool you came from.
- **You maintain the graph by hand.** Every new file, new dependency, new third-party library means editing a BUILD file. Forget a `deps` entry and the build breaks until you fix it. Tools like `gazelle` can generate BUILD files for some languages, but it's still upkeep that a `go build` or `npm install` never asked of you.
- **The ecosystem fights you at the edges.** Many libraries and IDEs assume the native tool of their language. Getting your editor, debugger, and that one weird dependency to play nicely with Bazel is real, recurring work - especially for languages where Bazel's rules are less mature.
- **It's a steep learning curve for the whole team.** Starlark, labels, configurations, toolchains, the `WORKSPACE`/`MODULE.bazel` seam - this is a lot of surface for every engineer to absorb, not a tool one person installs and forgets.

```console
$ bazel build //... --config=remote   # warm cache, large repo
INFO: 4213 processes: 4102 remote cache hit, 111 internal.
INFO: Elapsed time: 38.6s
```

*What just happened:* Out of 4,213 actions, 4,102 were served straight from the remote cache and never re-run. That ratio is Bazel at its best - and it only exists because someone paid the setup and discipline costs above. With no shared cache, those 4,102 actions would have run locally.

## When Bazel is worth it, and when it's overkill

This is the decision that matters more than any flag. Bazel earns its complexity under a specific shape of problem:

**Reach for Bazel when:**
- You have a **large monorepo** where full rebuilds are painfully slow and most changes touch a small slice.
- You build **multiple languages** in one repo and want one consistent build and test story across them.
- A **team or CI** can share a remote cache, so the per-build win multiplies across many people.
- **Reproducibility is a hard requirement** - regulated builds, supply-chain integrity, "this exact binary, byte for byte."

**Skip Bazel when:**
- It's a **single-language project** and that language's native tool (Cargo, Go, npm, Maven) already does incremental builds well. You'd be adding a second build system on top of a good one.
- The repo is **small or solo** - there's no shared cache to amortize the setup, and cold starts dominate. The complexity buys you nothing.
- Your team **can't or won't invest** in learning and maintaining it. A build system everyone fights is slower in human time than a simple one everyone understands.

> The trap is adopting Bazel because a famous company uses it. They have ten thousand engineers, a hundred-million-line repo, and a team whose whole job is the build. If your reality doesn't look like that, the native tool you already have is very likely the right call. Bazel solves a scale problem; if you don't have the scale problem, you've bought the cost without the benefit.

## In the wild

The pattern to copy from Bazel even if you never adopt it: **make your build's inputs explicit and your outputs reproducible.** That principle - declared inputs, content-hashed caching, isolated build steps - shows up across modern tooling and underpins the broader ideas in [build and release basics](/guides/build-and-release-basics). Bazel is the maximalist version of a good habit. You can practice the habit at any scale; you only need the maximalist tool when the scale demands it.

```quiz
[
  {
    "q": "What makes Bazel's remote cache trustworthy across a whole team?",
    "choices": [
      "Every developer runs identical hardware",
      "Hermetic actions are keyed by an input hash, so a cache hit is provably the same result",
      "Bazel re-verifies each downloaded artifact by rebuilding it",
      "The cache is encrypted end to end"
    ],
    "answer": 1,
    "explain": "Hermeticity makes an action's output fully determined by its inputs, so it can be hashed and shared safely. A non-hermetic action would poison the cache."
  },
  {
    "q": "Which situation is the WEAKEST fit for adopting Bazel?",
    "choices": [
      "A large multi-language monorepo with slow full rebuilds",
      "A team sharing a remote cache across CI and developers",
      "A small single-language project whose native tool already does incremental builds well",
      "A regulated project requiring byte-for-byte reproducible builds"
    ],
    "answer": 2,
    "explain": "For a small single-language project with a good native tool, Bazel adds setup and maintenance cost with no shared-cache or scale benefit to offset it."
  },
  {
    "q": "Why can a fresh checkout's first Bazel build sometimes be slower than a simpler tool?",
    "choices": [
      "Bazel intentionally throttles the first run",
      "With an empty cache there is nothing to reuse, so it builds the world from scratch",
      "Hermetic sandboxing is always slower than non-sandboxed builds",
      "It must download the entire Bazel source code first"
    ],
    "answer": 1,
    "explain": "Bazel's speed comes from incremental and shared caching. A cold start with no cache has nothing to skip, so it pays the full build cost up front."
  }
]
```
