Turborepo · advanced

Turborepo cache miss: determine whether inputs or environment changed

Diagnose Turborepo cache misses by separating input-graph changes from environment drift, using the task's hash metadata and Turbo's own logging to isolate whether files, env vars, dependencies, or daemon state invalidated the entry.

The symptoms

  • turbo run <task> prints "cache miss" or ">>> FULL TURBO" and re-executes the task even on a workspace where it previously hit cache.
  • Identical commands produce a cache hit on one developer machine but a miss on CI for the same commit hash.
  • Logs include lines such as "cache miss (input hash changed)" or "cache miss (env changed)" rather than the generic "cache miss" string.
  • Local builds hit cache only when the daemon is restarted, or only when run with turbo run --no-daemon.
  • Cache hits collapse to misses immediately after editing dotenv, .env, or files outside declared inputs.

Likely causes

  • Input hash drift: files matched by the task's `inputs` glob changed, or a previously implicit input was not declared.
  • Environment drift: values of keys in the task's `env` or `passThroughEnv` arrays differ between the two runs (CI vs local, branch vs main, shell vs CI runner).
  • Dependency hash drift: an upstream task declared in `dependsOn` changed its outputs or hash inputs, forcing downstream re-execution.
  • Output and global hash drift: `.turbo/cache` was cleared, the workspace moved, or `globalDependencies` (e.g., turbo.json) was modified.
  • Cache-store mismatch: local cache uses filesystem or `Turborepo_MMAP_AUTH` while CI uses a remote V8/Remote cache that lacks the prior artifact, producing a miss on one side only.

First ten minutes

  1. 01Reproduce on demand: run `turbo run <task> --summarize` twice in a row from a clean state; record whether the second run reports "cache hit" or "no cache entry".
  2. 02Set `TURBO_LOG_VERBOSITY=debug` (or pass `--log-level=debug`) so each task prints the hash inputs it computed, per the caching docs.
  3. 03Diff the two runs: capture the task's printed `hash` and the named reason for any miss (`inputs changed`, `env changed`, `output missing`, etc.).
  4. 04Inspect `turbo.json` for that task's `inputs`, `outputs`, `env`, `passThroughEnv`, and `dependsOn` so you know which fields feed the hash.
  5. 05Verify the cache store: confirm both runs resolve the same `TURBO_API` / `TURBO_TOKEN` / local cache directory; a mismatch between local-only and remote cache is itself a cause.

Evidence to collect

  • Full Turbo debug log from one cache-hit run and one cache-miss run of the same task on the same commit.
  • Resolved value of `turbo.json#tasks.<task>.inputs`, `.outputs`, `.env`, `.passThroughEnv`, and `.dependsOn` for the affected task.
  • List of files actually changed between the two runs (`git diff --name-only <shaA> <shaB>`) cross-checked against the declared `inputs` glob.
  • Environment snapshot (`env | sort` filtered to keys named in `env`/`passThroughEnv`, or `printenv`) on both runners to find drifted values.
  • Remote-cache reachability proof: HTTP 200 from the configured `TURBO_API` endpoint showing the artifact either present or absent for the computed hash.

Where to look

  • Boundary: the Turborepo hash-computation layer for a single task, not the task's own script. The decision is made before the script runs.
  • Boundary: `turbo.json` schema fields `inputs`, `outputs`, `env`, `passThroughEnv`, `globalDependencies`, and `globalEnv`.
  • Boundary: filesystem inputs matched by `inputs` globs, including dotfiles in workspace roots that Turbo hashes by default.
  • Boundary: process environment inherited by the Turbo CLI, filtered by the task's `env` declaration before hashing.
  • Boundary: the cache backend (local `.turbo/cache` vs a remote V8 cache) that decides whether a hash maps to a stored artifact.

Diagnostic steps

  1. 01Run the failing task twice with `--summarize --log-level=debug` and diff the per-task `hash` field. Identical hashes with a miss point at the cache store; different hashes point at input/env drift.
  2. 02If hashes differ, parse the debug log for the named input sets (`files`, `dependencies`, `env`, `global`, `task`) to see which set changed. The Turborepo caching docs describe these input groups.
  3. 03For `files` changes, compare `git diff --name-only` against the declared `inputs` glob. An undeclared file inside the glob range will still be hashed; a declared file outside the glob will not.
  4. 04For `env` changes, print the filtered environment on each runner using only the keys listed in `env`/`passThroughEnv` and diff them; Turborepo only hashes the keys you declare.
  5. 05For `dependencies` changes, run `turbo run <task> --dry=json` and inspect the dependency graph to find which upstream task's hash differs.
  6. 06For cache-store issues, run `turbo run <task> --summarize --log-level=debug` with `--cache` options enabled and confirm whether the log shows a remote lookup; absence of remote calls implies local-only cache.
  7. 07If only the daemon is involved, retry with `--no-daemon`; if the miss disappears, the daemon was holding stale task-graph state and the issue is environmental, not in `turbo.json`.

Common mistakes

  • Adding files to `inputs` "to be safe" — this expands the hash surface and turns previously cacheable tasks into misses; only declare inputs the task actually reads.
  • Treating any "cache miss" log line as a remote-cache failure without checking whether the local hash itself changed first.
  • Assuming two runners with the same commit will produce the same hash — env vars in CI (e.g., `CI`, `BUILD_NUMBER`) often differ and must be listed in `env`/`passThroughEnv` or excluded.
  • Comparing only script exit codes; Turbo's miss decision is made before the script executes, so a script-level fix cannot recover a missed entry.
  • Hashing files via `.gitignore`-style exclusions without declaring them in `inputs`; the hash walks the filesystem based on the glob, not on ignore rules.

Safe fixes

  • If evidence shows a real file change inside the declared `inputs` glob: accept the miss as correct behavior; no fix required, document the input boundary in `turbo.json`.
  • If evidence shows an env var drift not relevant to outputs: add the var to `passThroughEnv` so Turbo ignores it, or remove it from `env` if it was mis-declared; verify by re-running and confirming the hash is unchanged.
  • If evidence shows an undeclared file forcing invalidation: extend the task's `inputs` glob only if the file genuinely affects outputs; otherwise leave the default and confirm the miss is acceptable.
  • If evidence shows the daemon held stale state: switch to `--no-daemon` for diagnosis, then restart the daemon and re-run; do not change `turbo.json` for a daemon-only symptom.
  • If evidence shows remote-cache absence for an otherwise valid hash: confirm `TURBO_API` and `TURBO_TOKEN` resolve to the same team on both runners before changing any task definition.

Prove the fix

  1. 01Two consecutive `turbo run <task>` invocations on an unchanged tree report "cache hit" with identical `hash` values in `--summarize` output.
  2. 02The debug log shows no `inputs changed`, `env changed`, `output missing`, or `dependencies changed` reason between the two runs.
  3. 03Re-introducing the originally drifted input (file edit, env var change) reproduces a miss with the matching `cache miss (<reason>)` line, confirming the fix is targeted and not accidental.
  4. 04CI and local runners produce the same `hash` string for the same commit when filtered env and declared inputs are aligned.
  5. 05Remote-cache presence can be demonstrated via the configured `TURBO_API` returning HTTP 200 with the artifact metadata for the recorded hash.

Prevention and next steps

  • Maintain a written contract per task in `turbo.json`: list the exact `inputs` glob, `outputs`, `env`, and `passThroughEnv`, and review it whenever the task script changes.
  • Standardize CI environment by exporting only the variables that should influence hashing, and declare every required one in `env`.
  • Treat cache-miss reasons as first-class signals: monitor for `inputs changed`, `env changed`, and `dependencies changed` lines in CI logs to detect drift early.
  • Keep `.turbo/cache` location and remote-cache team configuration identical between local dev and CI to avoid store-side misses masquerading as input misses.

Safe commands and checks

turbo run <task> --summarize --log-level=debug
turbo run <task> --no-daemon --summarize --log-level=debug
turbo run <task> --dry=json | jq '.tasks[] | {task, hash, inputs}'
TURBO_LOG_VERBOSITY=debug turbo run <task> --summarize
git diff --name-only <shaA> <shaB>
printenv | grep -E '^(CI|BUILD_NUMBER|TURBO_)' | sort
grep -E '\"(inputs|outputs|env|passThroughEnv|dependsOn)\"' turbo.json