5.4.4 Stamping Cache Impact

recommended

Stamping does not make an entire build uncacheable. It adds workspace-status files as inputs to particular actions, and only those actions—and their downstream consumers—can lose reuse when the relevant status changes. The performance failure is therefore a blast-radius problem: frequently changing data placed in stable status repeatedly invalidates every stamped action that consumes it.

For rule-family integration patterns after the blast radius is understood, see H.11.2 Artifact Identity.

Status changes have a bounded cache blast radius
Only declared consumers enter the invalidation path. Unrelated actions keep their cached results.
STABLE STATUS
STABLE_RELEASE = release-b
A change reruns actions that declare stable-status.txt, so they can embed the new value.
VOLATILE STATUS
BUILD_TIMESTAMP = …
A change alone does not rerun a consumer. The embedded value may remain stale until another input changes.
stable-status.txt changes
declared input changed
Stamped consumer reruns
only if it consumes that file
Changed output propagates
downstream consumers may rerun
Affected path: status consumer → changed output → downstream invalidation
Unrelated path: no status-file input → cached result remains reusable
Keep development builds unstamped when provenance is unnecessary. Enable stable, output-determining metadata in an explicit release configuration.

Trace the data before blaming the cache

--workspace_status_command=... runs a program at the beginning of each build. The program prints key-value lines, which Bazel separates into two generated files. User-defined keys beginning with STABLE_ go to bazel-out/stable-status.txt. Other user-defined keys go to bazel-out/volatile-status.txt.1

Those files do not affect every action automatically. A rule must support stamping and arrange for one or both files to be consumed. Native and ecosystem rules differ in their stamp attributes, defaults, and implementation details. a custom rule can also declare ctx.info_file and ctx.version_file as action inputs directly. Check the rule's documentation or inspect the action rather than inferring behavior from the target's language.1,2

This gives a useful diagnostic chain:

workspace status key changes
        ↓
stable-status.txt changes
        ↓
an action that declares that file as an input is invalidated
        ↓
changed stamped output may invalidate downstream actions

The first two arrows do not imply the third for an unstamped target or for a rule that never consumes the status file. This is why “nothing caches” is a misleading diagnosis. The affected set can still be large—for example, if many release binaries embed the same changing identifier—but it is not universal.

At the action-cache boundary, treat stable-status.txt like an ordinary declared input: its content can participate in the cache lookup for an action that consumes it. Do not confuse that lookup with the ActionKey printed by aquery. 5.2.3 bazel aquery — Action Graph explains why that field is not the complete input-content or remote-cache key. This is the same affected-set reasoning used for ordinary source edits in 5.4.3 Measuring Granularity Trade-offs: find the actions that consume the changed input, then trace changed outputs into downstream actions.

Stable means “invalidate when this changes”

The stable/volatile split is a rebuild policy, not a judgement about whether a value is important.

Status classUser key spellingEffect when only this status changesAppropriate data
Stablestarts with STABLE_invalidates actions that depend on the stable filerelease version or source revision whose change must produce a new artifact
Volatileany other user keyBazel updates the volatile file but deliberately does not invalidate dependent actions for that change alonetimestamp, CI attempt, or other observation that may change every invocation

Bazel supplies several built-in BUILD_* keys, including volatile BUILD_TIMESTAMP and FORMATTED_DATE. But BUILD_ is not the syntax that makes a user key volatile: every user key without the STABLE_ prefix is put in volatile status.1

The volatile exception has a consequence that is easy to miss. If only volatile-status.txt changes, Bazel reuses the existing action result, so the already-built artifact keeps the older embedded value. When another genuine input changes and the action runs again, it sees the current volatile file. Volatile status therefore means “use the latest value on the next necessary execution,” not “regenerate the artifact on every build.”1

The stamping-status experiment makes that policy observable rather than inferred. Its workspace_status.sh reads independently controllable stable and volatile values. The custom versioned rule explicitly consumes ctx.info_file and ctx.version_file. This narrowly scoped experiment does not claim that --stamp automatically affects custom rules or that every language rule consumes both files.

Run the deterministic run_cache_experiment.sh from the snippet root. It uses a fresh --output_base, writes a separate JSON execution log for each invocation, and counts executions of the experiment's StampedReport action:

bash tools/run_cache_experiment.sh
1 initial                    executions=1 output=release-a/attempt-1
2 volatile-only change       executions=0 output=release-a/attempt-1
3 stable change              executions=1 output=release-b/attempt-2
4 volatile-only change       executions=0 output=release-b/attempt-2
5 ordinary input change      executions=1 output=release-b/attempt-3

The measured blast radius is one action in this workspace. Builds 2 and 4 show reuse despite changed volatile status, including the intentionally stale value in the output. Build 3 proves that a stable change invalidates the consumer. Build 5 changes an ordinary declared input, executes the action, and only then captures the latest volatile value. In a real repository, repeat this method for each suspected consumer and its downstream actions. Do not extrapolate from one target to “the whole build.”

think

Decide: A release archive must contain the exact source revision, while its build timestamp is merely diagnostic. Which status class should each use?

Reveal

Use a stable key such as STABLE_GIT_COMMIT for the revision: changing commits must invalidate the archive-producing action. Use a non-STABLE_ key for the timestamp if a timestamp change alone must not rebuild it. If every release must contain the time of that invocation, then the timestamp is a real output-determining input and the resulting cache loss is intentional. Changing its prefix cannot provide both a fresh timestamp and reuse of the old artifact.

Separate development and release configurations

The command-line --stamp and --nostamp flags work together with each rule's stamp attribute. For rules whose attribute delegates to the command line (commonly represented as stamp = -1), --stamp enables stamping and --nostamp disables it. A rule can instead force stamping on or off, and Bazel does not stamp binaries built in the exec configuration. Tests commonly default to stamping off. These details are rule-specific, so --stamp is not a promise that every target will consume status, and --nostamp cannot override a rule that forces it on.1

A practical default is to keep ordinary development builds unstamped and enable both the status command and stamping in an explicit release configuration:

build:release --stamp
build:release --workspace_status_command=tools/workspace_status.sh

That avoids running repository-inspection commands on every development build and keeps volatile release metadata out of the development action graph. It also makes the cache trade-off visible at the call site:

bazel build //app:server
bazel build --config=release //app:server

Whether a Git commit belongs in stable status deserves a deliberate answer. For a release artifact that promises to identify its exact source, changing the commit should invalidate the stamped action. For a local artifact whose bytes need not carry provenance, disabling stamping is usually better than relabeling the commit as volatile and accepting stale embedded metadata. 3.2.3 Stamping & Build Metadata shows how to wire the status command and rule-facing stamping controls. The consultant's task here is to verify the invalidation boundary.

Diagnose repeated execution with evidence

When a stamped build executes more work than expected:

  1. Run the same target and configuration twice with --explain=<file> and --verbose_explanations, then inspect the explanation for actions whose inputs changed. 2.4.4 Diagnosing Cache Misses develops that workflow.1
  2. Capture the status command's output twice. Look first for timestamps, random IDs, dirty-tree markers, hostnames, or CI attempt numbers under STABLE_.
  3. Inspect the suspect action with aquery to see whether a workspace-status artifact is among its inputs. Remember that aquery describes the planned action. It does not prove a cache miss.
  4. Check the rule's stamp contract and the effective flags. Do not assume that a stamp attribute has identical values or defaults across rulesets.
  5. Fix the policy, then repeat the same two-build experiment. Move data to volatile status only when stale-until-next-execution semantics are correct. otherwise keep it stable and narrow which actions consume it.

The last distinction matters most. A changing stable value is not inherently a bug. It is costly only when its rebuild semantics are unnecessary or its consumer set is wider than the artifact contract requires.

For a larger invocation, the execution logs establish which actions actually ran. A trace profile or 5.4.5 Bazel Invocation Analyzer (BIA) can then help quantify where those misses land in elapsed time. Keep causality in that order: first prove invalidation and its affected set, then assess its timing impact.

key takeaway

Stable status invalidates only actions that consume it, not the entire build. Put a value under STABLE_ when changing it must produce a new stamped artifact. put frequently changing observational data in a non-STABLE_ key only when it may remain stale until some other input causes execution.

Treat --stamp behavior as rule- and configuration-dependent. Diagnose the actual consumer action and its effective flags, and prefer a separate stamped release configuration over paying the provenance cost in every developer build.

Check your understanding · 4 questions

1.Match each workspace-status key to its rebuild policy:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
STABLE_RELEASE_VERSION
BUILD_TIMESTAMP
CI_ATTEMPT

2.For a build where one stable status value changes, classify each claim:

Choose True or False for each sentence

An action that does not consume stable-status.txt can still reuse its cached result.
Every stamped action necessarily consumes both workspace-status files.
A changed stamped output can invalidate downstream actions that consume it.
The stable status change makes the entire build globally uncacheable.

3.A release artifact must identify its exact commit, but local development artifacts need no embedded provenance. Which policy best preserves correctness and reuse?

Select one answer

4.Which steps provide evidence for diagnosing repeated execution in a stamped build?

Select all that apply

0 of 4 answered

Footnotes

  1. Commands and Options — Bazel 9 workspace-status partitioning, stable and volatile invalidation contracts, and the interaction between --stamp and rule attributes 1 2 3 4 5 6

  2. ctxctx.info_file and ctx.version_file expose stable and volatile workspace-status files to Starlark rule implementations