Verify your cache
Prove the cache is actually serving hits rather than quietly falling back to local work, using your build tool's own accounting, not a status page.
Who this is for: anyone who has pointed a build at NativeLink and wants evidence it is working, rather than the absence of an error. What you'll have at the end: a measurement you can repeat, a number you can put in a message to your team, and the ability to tell a cold cache from a broken one. Time: about ten minutes.
Before you start
A running NativeLink and a build pointed at it. See the Quickstart and Connect your build.
A misconfigured remote cache almost never fails loudly. Bazel's default is to treat cache errors as soft: if the endpoint is unreachable, the digest function does not match, or the instance name is wrong, it logs a line you will not notice and builds everything locally. The build succeeds. It is just not using the cache.
"No errors" proves nothing. What proves something is the client's own accounting of where each action's result came from.
There is no /metrics endpoint to curl
NativeLink exports telemetry over OTLP and nothing else: there is no HTTP
metrics route in the binary, and the /status path a health service
mounts only reports whether the process is up and its stores pass a
self-check. It will happily return OK for a server no client is
successfully talking to. Use it as a liveness check and nothing more; see
Observability for the real telemetry
path.
The measurement
The test is a build, a clean, and the same build again. The first populates the cache; the second must not do the work.
Build once, cold
bazel build //... \ --remote_cache=grpc://127.0.0.1:50051 \ --remote_instance_name=mainRead the last line of the output. It looks like this:
INFO: 1,284 processes: 81 internal, 1,203 linux-sandbox.Every process is local, which is what you expect: nothing was in the cache yet. If this run reports remote cache hits, something built these outputs before. That is fine, but you are no longer measuring a cold start, so pick a target nobody has built or change a source file.
Discard the local results
bazel cleanNot
bazel clean --expunge, which also throws away the external repositories and turns the next step into a ten-minute fetch that tells you nothing about the cache.Build again, warm
bazel build //... \ --remote_cache=grpc://127.0.0.1:50051 \ --remote_instance_name=mainNow the same line should read differently:
INFO: 1,284 processes: 1,203 remote cache hit, 81 internal.That number,
remote cache hit, is the whole measurement. It is the count of actions Bazel did not run because NativeLink handed back a result.
You did it right if
- The second build's
processes:line reports a non-zeroremote cache hitcount. - The second build is substantially faster in wall time than the first.
- The store's
content_pathon the server has grown; for the shippedbasic_cas.json5that is/tmp/nativelink/data-worker-test/content_path-cas(inside the container on the Docker path), and the action cache is beside it undercontent_path-ac. - Stopping NativeLink and re-running the build makes the hits disappear. This
one matters more than it looks: it is the only check that proves the hits
were coming from this server rather than from a
--disk_cacheon the client.
Reading the number honestly
A hit rate below 100% on an unchanged tree is normal. Bazel counts several kinds of work in that line, and only some of it is cacheable:
internal actions (symlink trees, file writes, workspace status) never go
to a remote cache. They are cheap and they will always appear.
Actions whose inputs genuinely changed are supposed to miss. If you edited a header between the two runs, everything downstream of it will rebuild, and that is the cache working correctly rather than failing.
Actions that are not deterministic will miss forever. A rule that embeds a timestamp, an absolute path, or a hostname produces a different action digest on every machine and every run, so it can never hit. If your rate is stuck low on an unchanged tree, this is usually why, and Correctness and hermeticity is the page that explains how to find them.
What should worry you is zero hits on a second run of an unchanged tree. That is not a tuning problem; that is the cache not being used at all.
Getting more detail than the summary line
When the summary is not enough, ask Bazel for the per-action record:
bazel build //... \
--remote_cache=grpc://127.0.0.1:50051 \
--remote_instance_name=main \
--execution_log_compact_file=/tmp/exec.logThe execution log records, for each action, whether it was a cache hit and what its digest was. It is the tool for answering "why did this specific action miss": compare the digest for the same action across two runs, and if it changed, the action's inputs or command line are not stable.
For wire-level questions (is the client even reaching the server, is it
calling FindMissingBlobs) turn on gRPC logging instead:
bazel build //... --remote_cache=grpc://127.0.0.1:50051 --remote_grpc_log=/tmp/grpc.logAn empty log means Bazel never opened a connection, which points at the flag rather than at the server.
Other build tools
The measurement is the same everywhere: build, clean, rebuild, and read the
tool's own accounting. Only the vocabulary changes. Buck2 shows where each
action ran in buck2 log what-ran; Pants counts remote cache requests in
its end-of-run stats when [stats].log is on; recc logs an
Action Cache hit line per compile with RECC_LOG_LEVEL=info. Look for
whatever your tool calls a cache hit rather than for a NativeLink-side signal;
the server cannot tell you whether the client asked it the right question.
The flags for Bazel, Buck2, Pants, Siso, and CMake with recc, now that you know how to tell whether they worked.
SidewaysRemote executionA cache serving hits is the prerequisite for running the actions themselves somewhere else.