cswork / libraryOpen practice workspace

Articles

Bazel Interview Questions: Hermetic Builds, Dependencies, and Remote Caching

Practice Bazel hermetic builds, hidden dependencies, action keys, remote cache safety, and reproducibility through a concrete build fixture.

A Bazel interview rarely stops at “the build is cached.” A useful answer identifies exactly which action inputs and tools determine an output, then asks whether local and remote execution see the same world. One hidden file read can make a fast cache hit wrong. This guide uses five original prompts, an action-key thought experiment, and a debugging checklist. They are practice exercises, not claims about an employer's interview. Hash a Repository with a Merkle Tree and Find Changed Files is useful adjacent practice for understanding why declared input identity matters.

Model a build as a graph of declared actions

The Bazel build concepts reference describes targets, dependencies, and actions. An action has a command, declared inputs and outputs, and an execution environment. The remote-caching documentation explains that action results and content-addressed output blobs can be shared across machines. The cache is safe only when the declared action identity captures everything that can affect the result and the action behaves reproducibly. Calling a build “hermetic” is an assertion about that boundary, not a synonym for “I used Bazel.”

Original practice fixture: a generated version.txt should contain a product version from checked-in release/version.json. The rule declares a template and an output, but its shell command also reads /tmp/current-version on the developer's laptop. Developer A's file says 4.2; developer B's says 4.3. Bazel cannot include the undeclared /tmp file in the action identity. Depending on sandbox and execution strategy, the action may fail to read it, or local unsandboxed execution may read it and produce an output whose cache key omits the relevant input. The exact behavior is environment dependent; the invariant is that the rule is wrong.

ChangeDeclared in action?Expected safe behavior
release/version.json changesYes, after fixing the ruleAction key changes; output is recomputed
/tmp/current-version changesNo in the flawed ruleHidden dependency risks incorrect incremental or shared-cache reuse
Compiler or generator version changesMust be declared/pinned appropriatelyOutput identity changes or execution remains controlled

Question 1: What does hermetic mean in practical terms?

The Bazel hermeticity guide frames hermetic builds as isolated from uncontrolled host differences and dependent on known source, configuration, tools, and libraries. In an interview, translate that into a test: run the same action with the same declared inputs in two clean environments. If the bytes differ because one process read /tmp/current-version, the action was not properly isolated or specified. If the bytes differ only because an output embeds current time or random data, the action is nondeterministic even if its file dependencies are declared. Hermeticity and deterministic output are related but not identical diagnoses.

Ask what is part of the action identity: input file contents, output names, command line, relevant environment and toolchain. A hard-coded which protoc that picks an arbitrary host installation is suspicious. A network request during code generation is suspicious. A locale, timezone, or current date that changes generated bytes is suspicious unless controlled. The fix is not merely “turn on the cache”; it is to declare and pin the tool, model the input as a dependency, pass controlled environment values, and make the generator emit stable output. Different rule types and platforms have their own mechanisms, so describe the invariant before prescribing one flag.

If the interviewer asks “can sandboxing guarantee everything?”, say no universal guarantee. The Bazel sandboxing guide explains how sandboxing makes undeclared file access visible by restricting the execution root, and it also discusses platform limitations. Some host resources, environment variables, or external services can still influence an action unless controlled. A sandboxed success is stronger evidence than an unsandboxed success, but compare execution modes and inspect rule declarations before declaring the build portable.

Question 2: Why can a remote cache hit be incorrect?

The shared cache maps an action identity to result metadata and content-addressed output files. If two real-world executions depend on different hidden data yet Bazel computes the same key, a result produced on A can be reused on B even though B expected another output. This is a false semantic hit: the cache correctly found an entry for the declared action, but the declaration omitted a true input. Clearing the cache may temporarily change the symptom; it does not repair the rule. Worse, repopulating the same flawed key can spread another wrong result.

Walk the practice fixture: the rule's visible inputs and command line are unchanged. A builds first under an execution strategy that can read its /tmp file, producing 4.2. B asks for the same declared action and receives A's result from remote cache. B's hidden file says 4.3, but it was never represented in the key. Do not overpromise that this precise /tmp read works under every sandbox; on many setups the read fails, which is helpful. The interview point is the mismatch between actual dependencies and declared dependencies. Replace /tmp/current-version with the checked-in file and declare it in the rule.

Also consider the opposite symptom: a cache miss when you expected a hit. The key may legitimately differ because the command line, toolchain, environment, or input digest changed. Log the action identity and compare execution metadata before calling the cache broken. A remote cache is not a global memoization function over the target name alone. Two machines building a target with different configurations may correctly produce different action keys.

Question 3: How do target dependencies differ from action inputs?

A deps edge expresses build-graph relationships between targets; it does not automatically mean every file anywhere in a dependency's repository becomes an input to every downstream action. Rules choose which providers and artifacts become actual action inputs. The interviewer may show a BUILD file with a target depending on a code generator library but the action invoking a host tool directly. The dependency label looks reassuring, yet the action may still use the wrong binary. Trace the generated action, not just the labels printed in the BUILD file.

Use the version fixture: version.txt must depend on release/version.json in the rule's declared input set. Merely adding a top-level aggregate target that happens to mention the JSON file may not put the file into the generator action. A correct answer asks how the rule passes ctx.files or appropriate provider artifacts to the action and how the command references them. Explain which output path the action declares; writing an undeclared side file is another reproducibility bug. Then run an incremental build where only the version JSON changes and verify the action reruns.

Cycles in the target graph are a different error: Bazel cannot topologically order a dependency cycle. Hidden inputs are an edge missing from the graph; a cycle is an impossible declared graph. The related build-order questions help practice the distinction. Do not suggest cache eviction as a fix for a cycle, or a topological sort as a fix for a hidden /tmp read.

Question 4: How would you debug “works locally, fails remotely”?

First capture the failing action and its execution strategy: local sandboxed, local unsandboxed, or remote. Preserve the command, declared input manifest, toolchain, relevant environment, and output paths. Reproduce under a sandbox with remote cache reads disabled if necessary to force execution, then compare with a remote run. Look for absolute host paths, undeclared generated files, network calls, system-installed tools, hard-coded temporary directories, and permissions. If the remote action fails because an input is missing, add the dependency in the rule. If both executions succeed but outputs differ, compare byte content and nondeterministic metadata.

Do not diagnose from the final target line alone. A target may have dozens of actions, only one of which is nonhermetic. Narrow the action by using execution logs and a small target. Be careful with --nocache_test_results or cache controls: tests and action-cache behavior differ, and a flag chosen casually can hide the real problem. The core experiment is to force the suspect action to execute with identical declared inputs in two controlled environments and compare outputs. Document which cache reads and writes were enabled during each run.

If an action has already published a bad shared result, protect other users while fixing the rule. Depending on infrastructure, temporarily stop writing that action to the shared cache or disable remote caching for the affected target, then create a corrected action key by declaring the missing input/tool. Avoid “wipe the entire cache” as the first response. The sandboxing guide warns that incorrect cache reuse can affect many developers; the durable repair is a correct action definition.

Question 5: How do you test reproducibility without cargo culting flags?

Build the small fixture from a clean checkout on two machines or isolated containers using the intended toolchain. Vary one declared input and confirm the output and action key change. Vary one unrelated host condition—such as a local /tmp file—and confirm the corrected output does not change. Run locally and remotely with the same configuration, and compare output digests. Also test the cache path: one clean execution writes a result, a second clean execution reads it, and both deliver identical bytes. Record the exact Bazel version and remote-cache configuration so the experiment is repeatable.

This is stronger than saying “Bazel builds are reproducible.” A build can be mostly correct with one nonhermetic generator. A cache hit can make a broken action look fast and stable until a machine with different hidden state executes it. A meaningful interview answer identifies the action boundary, explains why the hidden input is absent from the key, proposes a rule-level fix, and proves the fix with a controlled variation. That is the bridge from build-system vocabulary to engineering judgment.

The interview answer in one minute

Draw target graph, action inputs, and cache result as three separate boxes. Name the undeclared dependency in the failing action. Explain whether sandboxing blocks it, whether a shared cache can reuse an incorrect result, and which declared input or pinned tool would repair the key. Then propose the two-machine reproducibility test. The answer should be about the actual action and its evidence, not an unqualified claim that remote caching is good or bad.

Challenge the easy cache diagnosis

One more experiment prevents a false conclusion. If A and B get different outputs with the same declared inputs while the remote cache is disabled, the action may be nondeterministic or reading a hidden host input. If A and B get identical output when forced to execute but one machine reports a cache miss, compare action keys and configuration before blaming correctness. A miss can be valid even when output bytes happen to match. If both machines report a hit and receive the same wrong bytes, inspect the rule's declared input boundary rather than treating the cache as a mysterious corrupt store. These observations separate cache behavior from action behavior.

Sources and further reading