How does remote build caching in tools like Bazel or Nx decide whether it can reuse a previous task's result instead of re-executing it, and what property does the task need to have for that reuse to be safe?
answer
- cache key = hash of inputs
- hermetic = no hidden inputs
- deterministic = same in -> same out
- local vs shared/remote cache store
- sandbox execution enforces hermeticity (Bazel)
basics
~20 sThe tool fingerprints everything that could affect a task's output — its source files, commands, and dependencies — into one hash. If a previous run already produced and uploaded a result for that exact hash, it downloads that instead of rerunning. This only works if the same inputs always produce the same output.
solid answer
~50 sEach task execution (compile, test, lint) is content-addressed: the orchestrator computes a hash over everything declared as an input — source file contents, transitive dependency hashes, the command/flags, and relevant toolchain versions — and uses that hash as a cache key. Before running the task, it checks a cache store (local disk, or a shared remote service like Bazel's Remote Cache protocol, Nx Cloud, or Turborepo's remote cache) for an entry under that key; on a hit, it downloads and replays the stored outputs and exit code without executing anything. This is safe only if the task is hermetic and deterministic — same inputs always produce byte-identical (or at least behaviorally identical) outputs, with no hidden dependency on network state, wall-clock time, uncontrolled environment variables, or machine-specific paths. Any of those violates the caching contract and produces incorrect cache hits.
go deeper
Should grasp the basic idea: identical inputs -> reuse the old result instead of rerunning, saving time.
Should describe the hash-of-inputs cache key mechanism and know that hermetic/deterministic tasks are a prerequisite for safe caching, with at least one named tool.
Should be able to diagnose a stale-cache-hit bug from symptoms (nondeterministic pass/fail pattern), name concrete hermeticity violations, and explain sandboxing as a mitigation.
Should reason about the org-wide economics of shared remote caching, when the investment in enforcing hermeticity (e.g., migrating to Bazel sandboxing) is worth the friction it adds to every engineer's workflow, and how to design task input declarations to avoid both false hits and needlessly broad invalidation.
## Content addressing: how the key is built Remote caching turns a task execution — compiling a library, running a test suite, linting a package — into a lookup problem instead of a compute problem, and the mechanism that makes that safe is **content addressing**. Before running any task, the orchestrator computes a cryptographic hash (Bazel and most modern tools use something like SHA-256) over a well-defined set of inputs: - the contents of every source file the task reads; - the hashes of every upstream dependency's outputs (so a change three layers down the graph still changes the hash at the top); - the exact command line and flags being invoked; - often the toolchain/compiler version. That hash becomes the **cache key**. ## The lookup, hit and miss The orchestrator then asks a cache store — which might be a folder on local disk for a single developer, or a shared remote service reachable by every CI runner and every teammate's laptop, such as Bazel's Remote Cache protocol, Nx Cloud, or Turborepo's remote cache — "has anyone already produced output for this exact key?" 1. **If yes**, it downloads the stored artifacts (compiled binaries, test reports, coverage data) and the exit code, and reports the task as complete without ever invoking the compiler or test runner. 2. **If no**, it runs the task for real and, on success, uploads the result under that key so the next person (or the next CI job, seconds later) gets a hit. ## Why it exists The reason this exists is straightforward **economics at scale**: the same unchanged code gets rebuilt and retested over and over — - once per CI run per branch, - once per developer's local iteration, - once per merge — and in a large team, the overwhelming majority of those executions are against inputs someone else has already built moments earlier. Sharing the cache turns thousands of redundant builds across a whole engineering org into effectively one real build per unique input state, with everyone else getting near-instant downloads. This is the single biggest lever these tools pull for CI cost and wall-clock time in large monorepos. ## The trade-off: the promise the task has to keep The trade-off is that the entire safety of this system rests on a promise the underlying task itself has to keep: **hermeticity and determinism**. - **Hermetic** means the task's output depends only on its declared inputs — no reading files outside the sandbox, no hitting the network, no depending on machine-specific absolute paths. - **Deterministic** means running it twice with identical inputs produces identical (or at least behaviorally equivalent) outputs — no embedding the current timestamp or a random UUID into a build artifact, no relying on filesystem iteration order that varies by OS. | Tool | How much of that promise it polices for you | |---|---| | **Bazel** | takes this extremely seriously: by default it executes each action inside a sandbox that only exposes declared inputs, actively working to catch violations at build time rather than trusting the build script's discipline | | **Lighter tools like Nx and Turborepo** | generally trust the task author to keep it hermetic and offer configuration to declare inputs/outputs explicitly, but don't sandbox execution the same way — faster to adopt, weaker guarantees | ## Failure modes Failure modes show up as a specific, confusing class of bug: "it works on my machine but CI is broken" inverted into "CI says it passed but the code is actually broken." - A test that reads an environment variable **never declared as a cache input** can pass once under one env configuration, get cached, and then silently "pass" forever afterward even when the underlying logic breaks, because the cache key never changes. - A code generator that **embeds a build timestamp** produces a different byte-for-byte output every run, which either destroys the cache hit rate (every run looks "new") or, worse, if the timestamp isn't part of what's checked for correctness, masks a genuine non-determinism bug. - Teams also hit **"cache key too broad"** problems, where a task declares more inputs than it actually needs (e.g., depending on an entire shared `utils` package when it only calls one function from it), so any unrelated change anywhere in that shared package invalidates the cache for everything downstream — correct, but needlessly conservative, and a sign the dependency graph itself needs finer-grained splitting. ## Where it shows up A concrete, widely cited real-world example: Google's internal build system (Bazel's ancestor, Blaze) was built specifically to make remote caching and remote execution trustworthy at the scale of a single-repo codebase with tens of thousands of engineers, precisely because hermetic, content-addressed caching was the only way to keep build times tractable; Bazel is the open-sourced descendant of that same design. On the JS side, Turborepo's marketing point of "never build the same code twice" is this exact mechanism, and its adoption by companies like Vercel is largely justified by the CI-minutes savings it produces once a monorepo passes a few dozen packages.
- What's the difference between local caching and remote/shared caching, and why does remote caching matter more for large teams?Local caching only benefits the single machine that produced a result — useful for a developer re-running the same task repeatedly, like `test` after a no-op edit. Remote caching uploads results to a service every machine can read, so if any CI job or any teammate has already built a given input state, everyone else gets a download instead of a rebuild, which is where most of the aggregate time savings come from at team scale.
- How does Bazel's sandboxing help enforce that a task's inputs are exactly what it declares?Bazel executes each action in an isolated environment (often a separate filesystem namespace or container) that only mounts the files explicitly declared as inputs for that action. If the build script tries to read a file it didn't declare, the read fails outright rather than silently succeeding, which surfaces hermeticity bugs at build time instead of as mysterious stale-cache incidents later.
- If two engineers on different operating systems (say macOS and Linux) build the same package, can they share cache hits?Only if the task output is genuinely platform-independent and the cache key accounts for platform as part of the input set when it isn't. Native compilation typically differs across platforms, so a well-designed cache key includes the target platform/toolchain, producing separate cache entries per platform rather than a false hit that serves a Linux binary to a macOS build.
Like a librarian who, before photocopying a document, checks whether an identical copy already exists on a shared shelf accessible to the whole building — but that only works if two 'identical' requests really do produce identical documents every time, not one with today's date stamped in the corner.
saying these in an interview costs you the question
- Describes caching as purely 'file didn't change so skip it' without mentioning command/flags/dependency hashes as part of the key
- Doesn't know the difference between local and remote/shared cache
- Assumes any task can be safely cached without qualifying it needs to be deterministic/hermetic
- Can't name a concrete way a task becomes non-hermetic (timestamps, env vars, network calls)
- Thinks caching is the same thing as incremental compilation inside a single compiler invocation