NativeLink

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.

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.

  1. Build once, cold

    bazel build //... \
      --remote_cache=grpc://127.0.0.1:50051 \
      --remote_instance_name=main

    Read 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.

  2. Discard the local results

    bazel clean

    Not 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.

  3. Build again, warm

    bazel build //... \
      --remote_cache=grpc://127.0.0.1:50051 \
      --remote_instance_name=main

    Now 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-zero remote cache hit count.
  • The second build is substantially faster in wall time than the first.
  • The store's content_path on the server has grown; for the shipped basic_cas.json5 that is /tmp/nativelink/data-worker-test/content_path-cas (inside the container on the Docker path), and the action cache is beside it under content_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_cache on 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.log

The 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.log

An 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.

NextConnect your build

The flags for Bazel, Buck2, Pants, Siso, and CMake with recc, now that you know how to tell whether they worked.

SidewaysRemote execution

A cache serving hits is the prerequisite for running the actions themselves somewhere else.

On this page