Bazel: Cache Mechanisms Demystified

2024/12/29

BUILDbazelcaching

You change one line in a monorepo with 10,000 targets. Bazel rebuilds exactly what’s affected and nothing else. That’s not magic – it’s caching. Several layers of it, each solving a different problem.

The terminology gets confusing fast. “Action cache” can mean two different things depending on context. So let’s walk through each layer, what it stores, and when it kicks in.

In-Memory Cache (Skyframe)

Bazel’s evaluation engine, Skyframe, breaks the entire build into fine-grained nodes in a DAG. Each node’s result gets cached in memory.

This is the fastest cache – no disk I/O, no serialization. But it lives in the Bazel server process. Kill the server, lose the cache.

That’s why Bazel runs as a daemon. The bazel command you type is a thin client that talks to a long-running server. The server holds the Skyframe graph in memory across commands. Run bazel build //foo, then bazel build //foo again – the second one is near-instant because Skyframe already has all the answers.

Output Base Action Cache

Bazel writes build outputs to the output tree (bazel-out/). But how does it know whether those outputs are still valid?

It keeps an index at $(bazel info output_base)/action_cache/. This index maps actions to their outputs using a custom binary format. You can inspect it:

bazel dump --action_cache

Here’s what an entry looks like:

String indexer content:

size = 2
bazel-out/stable-status.txt <==> 0
bazel-out/darwin_arm64-fastbuild/bin/a_1.txt <==> 1

Action cache (3 records):

0, bazel-out/stable-status.txt:
      actionKey = 709e80c88487a2411e1ee4dfb9f22a861492d20c4765150c0c794abd70f8147c
      usedClientEnvKey =
      digestKey = 7ee6a29fdf260221ff734cd170ca02c84a31051aeaaace7e573a39500ae0eac0

      packed_len = 106

1, bazel-out/darwin_arm64-fastbuild/bin/a_1.txt:
      actionKey = d7a4bc4e9aafcd9b7c035bb7f742bdd31441b34e0d0c4b6fc7b9a2bcf7b8dc22
      usedClientEnvKey =
      digestKey = 02669d6f7b53a6b306654962524d5838f217ef4101d0d687da2dbf3f624b66b3

      packed_len = 106

Three fields matter:

Field What it hashes
actionKey Non-file metadata: command line args, mnemonic
usedClientEnvKey Environment variables from --action_env
digestKey The action’s inputs and outputs

When Bazel considers running an action, it computes these digests for the current state and compares them against the cache entry. If everything matches, the output sitting in bazel-out/ is still valid. Skip the action.

External Action Cache

This is the cache people usually mean when they say “remote cache.” You enable it with --remote_cache (for a remote server) or --disk_cache (for a local directory that outlives bazel clean).

It has two stores:

Action Cache (AC)

The AC answers the question: “has this exact action, with these exact inputs, been run before?” If yes, it hands back the result without re-executing.

Content Addressable Store (CAS)

The CAS stores the artifacts themselves. The path is derived from the content hash – same content, same path, every time. When the AC says “this action already ran and produced these outputs,” the CAS is where Bazel fetches those outputs from.

Together: AC tells you what was produced, CAS gives you the thing that was produced.

A note on disk cache

You can see these concepts in action locally. Set --disk_cache=/tmp/bazel-disk-cache and build something twice. The second build pulls from AC+CAS instead of re-executing. Same mechanism as remote cache, just on your filesystem.

Repository Cache

This one is simpler. When Bazel downloads external dependencies (archives from HTTP), it stores them in a content-addressable cache keyed by SHA256 hash.

The flow

  1. Bazel needs an archive. It checks the repository cache for a matching SHA256 hash.
  2. Cache hit: unpack from cache, skip the download.
  3. Cache miss: download from the URL in your WORKSPACE or MODULE.bazel, verify the hash matches what you declared, store in cache.

Things to know

Putting it together

Four caches, four different scopes:

Cache Scope Survives bazel clean? Shared across workspaces?
Skyframe (in-memory) Server process No No
Output base action cache Output base No No
External (AC + CAS) Configurable Yes Yes
Repository cache Machine-wide Yes Yes

The further down the table you go, the broader the sharing and the longer the cache lives. Skyframe is the fastest but most ephemeral. The repository cache is the slowest but basically permanent.

Most of the time, you don’t think about these layers individually. You just notice that the second build is fast. But when it isn’t fast – when Bazel rebuilds something you know hasn’t changed – understanding which cache missed and why is how you fix it.