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)
- Key: digest of the action’s metadata and inputs
- Value: serialized
ActionResultprotobuf
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)
- Key: digest of the file content
- Value: the actual output file bytes
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
- Bazel needs an archive. It checks the repository cache for a matching SHA256 hash.
- Cache hit: unpack from cache, skip the download.
- Cache miss: download from the URL in your
WORKSPACEorMODULE.bazel, verify the hash matches what you declared, store in cache.
Things to know
- Enabled by default. Customize the location with
--repository_cache. - Survives
bazel cleanand evenbazel clean --expunge. - Shared across workspaces and Bazel versions – one download serves every project on the machine.
- Watch out for URL changes without hash updates. If someone bumps a dependency URL but forgets to update the SHA256, Bazel happily serves the old cached content. The hash matches, so it trusts the cache. This is correct behavior and also a footgun.
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.