Turborepo · intermediate
How to verify a Turborepo cache hit is safe
Verification playbook for determining whether a Turborepo "cache hit" is genuinely safe — meaning the restored outputs reproduce the same artifacts only when all relevant inputs (sources, dependencies, environment, configuration) match what produced them originally.
The symptoms
- •turbo run reports "cache hit" on a task whose source files appear to have changed since the last successful run, raising doubt about staleness.
- •Restored outputs differ from a fresh build in surprising ways: missing files, different hashes, or downstream tasks behave as if earlier packages were older than recorded.
- •CI passes locally only when the remote cache is bypassed, suggesting the remote artifact corresponds to inputs the local environment does not satisfy.
- •Engineers suspect that inputs outside the Turborepo task graph — environment variables, global config, or toolchain versions — were not factored into the cache key.
Likely causes
- •The task's declared inputs (sources, dependencies, env, outputs) do not enumerate every file that meaningfully affects the artifact, so two divergent inputs hash to the same key.
- •An environment variable was added to turbo.json's env list after the cached artifact was produced, meaning the new key correctly invalidates old hits but the comparison is being made against the wrong baseline.
- •Outputs of a task were changed in turbo.json (different globs), so a "hit" restores files that no longer match the declared output contract.
- •A transitive dependency's hash changed in the lockfile but the consuming task's inputs do not include the lockfile or the dependency package directory.
- •Remote cache is being used across machines with materially different toolchains (different Node, package manager version, OS) and the inputs don't capture that variation.
First ten minutes
- 01Reproduce on demand: run the suspected task with --force to obtain a known-fresh artifact and its hash; record the hash from the terminal output. Then run the same task without --force and compare the reported cache key.
- 02Re-run with --summarize and inspect the per-task hash, inputs list, and outputs list for the affected task.
- 03Compare the current turbo.json task definition against the version of turbo.json that existed when the suspect artifact was first cached (git log on turbo.json).
- 04Check whether the task is opted out of remote cache via --no-remote-cache, --remote-only, or TURBO_REMOTE_CACHE_DISABLED; a "hit" from the local cache layer has different trust implications than a remote hit.
- 05Inspect the .turbo/cache directory on the verifying machine to see what was actually restored (file presence, mtimes, sizes).
Evidence to collect
- •The exact cache key reported for the suspect task in a fresh run vs. a cached run (turbo run --summarize and turbo run <task> --force --summarize).
- •The resolved turbo.json task definition: inputs, outputs, env, dependsOn, and any passThroughEnv or globalEnv entries.
- •The full list of files Turborepo considers as inputs for the task (visible via --summarize or --dry-run=json).
- •Git status and the commit hash of HEAD at the time of the suspect cached build vs. the verifying build.
- •Environment variable values that affect the task (both declared in turbo.json and undeclared but plausibly relevant).
- •Lockfile hash (e.g., package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb) and the resolved versions of direct and transitive dependencies.
Where to look
- •turbo.json at the repo root, specifically the pipeline / tasks definition for the suspect task: inputs, outputs, env, dependsOn, outputs, globalEnv, globalDependencies.
- •The .turbo/cache directory under each workspace package and at the repo root — restored outputs land here and are copied into the package on hit.
- •The Turborepo daemon log if --daemon is enabled; the log records which inputs and outputs the hashing observed.
- •Remote cache metadata if remote caching is configured: the Vercel Remote Cache / custom cache server stores the hash alongside the artifact; the hash is what makes the hit identifiable.
- •package manager lockfile and the resolved dependency graph at the workspace root and the package level for transitive inputs.
Diagnostic steps
- 01Run the suspect task twice back-to-back with --summarize and confirm the second invocation reports a cache hit with the same hash; if the hash differs between runs, the inputs are not stable and no conclusion is possible yet.
- 02Re-run with --force to bypass cache, then re-run without --force; compare the produced artifact (file list, contents hash via a checksum tool) to the cached artifact restored from a prior run. If artifacts differ, the cache key is under-specified.
- 03Inspect --dry-run=json output to enumerate the exact inputs Turborepo hashed for the task; cross-check this list against files you believe should affect the output (tooling configs, generators, codegen steps).
- 04Audit the task's outputs globs in turbo.json; if outputs include paths the task does not actually write, or exclude paths it does, the restored file set can be incomplete or stale.
- 05Check globalDependencies and globalEnv at the top level of turbo.json; these are factored into every task's hash and a missed entry here causes cross-task cache poisoning.
- 06Compare the lockfile hash implied by the cached run vs. the current lockfile; if they differ but the task's inputs do not include the lockfile, the cache key is missing a dependency-affecting input.
- 07If remote cache is in use, verify whether the artifact was produced on a different OS/architecture/toolchain; mismatched toolchains can produce binary-different but input-equivalent artifacts.
Common mistakes
- •Treating a cache hit as proof of correctness: a hit only proves the inputs hashed to the same key, not that the inputs enumerated are complete.
- •Trusting remote cache artifacts without checking that the originating machine's toolchain matches; binaries and some generated files are not portable across OS or Node major versions.
- •Adding a new env var to the task but forgetting to clear the existing remote cache; old artifacts produced under different env will still match the new key only after the cache is pruned.
- •Modifying outputs globs in turbo.json without re-priming the cache; old artifacts may restore a file set that no longer matches the declared outputs.
- •Assuming dependsOn covers all relevant upstream effects; dependsOn affects execution order and hashing, but transitive source-file changes still require those files to be in some task's inputs.
Safe fixes
- •Expand the task's inputs in turbo.json to include every file that affects the artifact (tooling configs, lockfiles, generator templates, codegen inputs), then run with --force to re-prime the cache before declaring the hit safe.
- •Add the lockfile and any global tooling config to globalDependencies in turbo.json so a lockfile change invalidates every task's hash.
- •Tighten outputs globs so they precisely match what the task produces; avoid catch-all globs that restore stale files into a different directory layout.
- •For tasks that produce non-portable binaries, either disable remote caching for that task via a workspace-level override or document the toolchain requirement explicitly.
- •After any change to turbo.json inputs/outputs/env, run turbo run <task> --force on a clean checkout to produce a canonical artifact, then verify subsequent hits restore byte-identical outputs.
Prove the fix
- 01Re-run the suspect task twice with --summarize; the second run reports a cache hit with the same hash as the first, and the artifact restored from cache is byte-identical to a fresh --force run (verified via a checksum tool over the declared outputs).
- 02Modify a file that is in the task's input set and confirm the cache key changes; modify a file outside the input set and confirm the cache key is unchanged.
- 03Modify the lockfile and confirm every dependent task's hash changes (i.e., the lockfile is captured as an input, directly or via globalDependencies).
- 04Change an environment variable listed in the task's env and confirm the cache key changes; leave an unrelated env var unchanged and confirm the key does not change.
- 05If remote cache is enabled, run the task on a second machine with identical inputs and toolchain and confirm the second machine reports a hit restoring the same artifact.
Prevention and next steps
- •Adopt a convention that every task's inputs in turbo.json include the lockfile (or use globalDependencies), toolchain configs, and any codegen inputs; treat additions to this list as cache-invalidating changes that require re-priming.
- •Pin the Node and package manager versions used to populate the remote cache, and document the supported range so downstream machines don't consume artifacts they cannot reproduce.
- •Review turbo.json changes in code review with the same rigor as code changes: any modification to inputs, outputs, env, or globalDependencies can silently invalidate or mis-prime caches across the organization.
- •Periodically verify a sample of cached tasks end-to-end by forcing a re-run and comparing artifacts; this catches drift introduced by gradual config edits.
Safe commands and checks
turbo run <task> --summarize turbo run <task> --force --summarize turbo run <task> --dry-run=json turbo run <task> --no-remote-cache --summarize git log --oneline -- turbo.json git rev-parse HEAD