Turborepo · beginner

Turborepo cache checklist

Turborepo caches task output by hashing inputs (sources, env, dependencies) and outputs. When a task reports cache misses, partial hits, or stale results, the root cause is almost always that the task definition is missing inputs that actually affect its output. This checklist enumerates what to inventory in a `turbo.json` `pipeline` task before trusting any cache hit, ordered so the cheapest, most decisive checks come first. The guide targets Turborepo v1/v2 declarative `pipeline` tasks; `turbo.json` is the only configuration source of truth for the cache key.

The symptoms

  • The same package builds twice in a row with no source change and `turbo run <task>` logs `cache hit, replaying output` for some tasks but not the one under review.
  • Local cache hits but CI cache misses (or vice versa) because environment variables used by the task are not declared in the `env` array, so hash drift is invisible to `turbo`.
  • Outputs change after editing a file outside `src/` (e.g., a `tsconfig.base.json`, a workspace `.env`, a codegen config) yet the task is still served from cache.
  • `turbo run <task> --force` is added reflexively to "fix" correctness, masking missing `inputs`/`outputs` declarations.
  • Newly added workspace dependencies (`package.json`) do not invalidate dependents, so a downstream task's cache survives an upstream version bump.
  • `turbo run <task> --dry-run=json` shows `inputs` containing far fewer files than the task actually reads from disk.

Likely causes

  • `inputs` only list `src/**` or `package.json` but the task also reads root config files (`tsconfig*.json`, `jest.config.*`, `vitest.config.*`, `.eslintrc*`, `babel.config.*`, `postcss.config.*`), so changing those does not invalidate the hash.
  • `env` is omitted entirely or lists a partial allowlist (`env: ["NODE_ENV"]`) while the task shells out to `MY_API_TOKEN`, `CI`, `GITHUB_SHA`, or other variables captured implicitly by the toolchain.
  • `outputs` are missing, so Turborepo cannot restore or compare artifacts; this forces a re-run even when the hash matches and obscures whether the task is actually cacheable.
  • `dependsOn` is incomplete; a task reads a generated artifact from another package but does not declare `dependsOn: ["^build"]` (or the right upstream), so it caches independently of its real inputs.
  • Glob patterns are non-portable (shell-style vs. micromatch) or anchored incorrectly (`./src/**` vs `src/**`), silently excluding files.
  • `.turbo/cache` is shared across branches or workspaces via a misconfigured Remote Cache backend, so hashes from one environment poison another.

First ten minutes

  1. 01Run `npx turbo run <task> --dry-run=json` (read-only) and inspect the `tasks[].task` block for its `inputs` and `outputs` lists; these are exactly the files Turborepo will hash on the next real run.
  2. 02Open `turbo.json` and read the failing task's full definition: `inputs`, `outputs`, `dependsOn`, and `env`. A task with no `inputs` key hashes by default on package content, which is usually too narrow.
  3. 03From the package directory of the failing task, list every file the task actually reads: source, configs at the repo root that the toolchain walks up to find, dotfiles, and any `*.config.*` it loads.
  4. 04Cross-check that list against `dry-run`'s `inputs`. Any file the task reads but Turborepo does not hash is a candidate for a stale cache hit and must be added.
  5. 05Identify which environment variables the task's underlying tool consults at runtime (lint config, build tool flags, test runners) by checking the tool's docs and the package's scripts; these must be enumerated in `env`.
  6. 06Capture `git status --porcelain` and the current branch to make sure no uncommitted files are silently shifting the hash between dry-run and a real run.

Evidence to collect

  • The exact `inputs`, `outputs`, `dependsOn`, and `env` arrays for the affected task as emitted by `turbo run <task> --dry-run=json`.
  • Repo-root and package-level config files the task's tool reads (e.g., `tsconfig*.json`, `jest.config.*`, `vitest.config.*`, `.eslintrc*`, `babel.config.*`, `postcss.config.*`, `tailwind.config.*`).
  • Environment variables the underlying tool consults; documented from the tool's official configuration reference, not from memory.
  • List of workspace packages the task transitively depends on at runtime (sources of generated artifacts), mapped against the task's declared `dependsOn`.
  • The Remote Cache backend configuration (`TURBO_API`, `TURBO_TOKEN`, `TURBO_TEAM`) and where artifacts are stored; mismatched credentials across environments explain cross-env cache misses.
  • Current `git` HEAD and branch to correlate dry-run vs. real-run hash differences with uncommitted changes.

Where to look

  • `turbo.json` at the repo root — the only authoritative source for task hashing keys in declarative `pipeline` mode.
  • The package directory of the affected task: `package.json` (especially `scripts` and `dependencies`/`devDependencies`) and any tool config files it loads.
  • The repo root for shared config files (TypeScript base, ESLint flat config, Prettier, Jest projects, Tailwind, PostCSS) that any package's tool may transitively load upward.
  • The `.turbo/cache` directory boundary and the Remote Cache backend's artifact store to confirm hashes are scoped to the correct environment.
  • CI provider's variable/scope settings where env vars like `CI_COMMIT_SHA`, `GITHUB_SHA`, or signing tokens are injected without being declared in `env`.

Diagnostic steps

  1. 01Issue `npx turbo run <task> --dry-run=json` and compare the emitted `inputs` glob set against the actual files the underlying tool reads (verified by enabling the tool's own dependency resolution flag, e.g., `tsc --listFiles`). Mismatch = the hash is incomplete.
  2. 02For each root-level config file the tool loads, decide whether it belongs as a literal entry (`"tsconfig.base.json"`) or a glob (`"*.config.json"`); missing entries are the most common cause of stale hits after a config edit.
  3. 03Cross-reference every env var the tool's docs state it reads with the task's `env` allowlist; any undeclared var creates env-dependence invisible to the hash.
  4. 04For tasks that consume generated artifacts, confirm `dependsOn` includes the upstream task name (use `^name` for the same package's own prior run and a bare name for cross-package deps).
  5. 05Temporarily set `cache: false` on the suspect task in `turbo.json`, re-run, and confirm the failure or wrong output disappears; if behavior changes, the cache was masking a real input gap.
  6. 06Confirm `outputs` are declared and the directories actually exist on disk after a real run; otherwise Turborepo cannot record or restore artifacts and the hash's value is reduced.
  7. 07Verify the Remote Cache backend (`TURBO_API`, `TURBO_TEAM`, `TURBO_TOKEN`) is identical across local and CI; divergent backends yield divergent hashes that look like misses but are actually isolation.

Common mistakes

  • Assuming Turborepo hashes "everything in the package" — by default a task without `inputs` hashes only source files Turborepo detects; declaring `inputs` explicitly is required to make the contract auditable.
  • Trusting `cache: "local"` in CI; in Turborepo v1/v2 the mode string accepts `"local"`, `"remote"`, or omitted, but Remote Cache requires backend credentials, otherwise hits degrade to local-only and silently miss across CI nodes.
  • Adding `inputs: ["**"]` to force "everything counts" instead of enumerating; this slows hashing, hides missing dependencies, and still misses root config files outside the package.
  • Forgetting the `^` prefix on `dependsOn` for tasks that need the same package's prior run completed; the hash will not include the upstream output.
  • Treating `--force` as a fix: it bypasses the cache entirely and hides the real input-gap bug from code review.
  • Mixing shell-style globs (`src/*.ts`) inside Turborepo config where micromatch-style (`src/**/*.ts`) is required; the patterns look similar but exclude different file sets.

Safe fixes

  • Conditional on dry-run evidence: if `inputs` is missing config files the tool loads, add them as explicit entries (literals or scoped globs) so the hash invalidates on those edits.
  • Conditional on env audit: if the task's tool reads undocumented env vars, list them in `env` (the explicit allowlist contract) so changes invalidate the cache instead of producing silent stale hits.
  • Conditional on dependency graph: if the task reads generated artifacts from another workspace, add the upstream task name to `dependsOn` using `^name` only when the dep is in the same package.
  • Conditional on outputs evidence: if `outputs` is missing, declare the artifact directories actually written (e.g., `["dist/**", ".next/**", "build/**"]`) so Turborepo can record and restore them.
  • Conditional on Remote Cache isolation: if local and CI backends differ, align `TURBO_API`/`TURBO_TEAM`/`TURBO_TOKEN` to a single backend before changing task definitions.
  • Each fix is gated on the corresponding evidence above; do not apply changes speculatively.

Prove the fix

  1. 01Run the same task twice in a row on identical source; observe `cache hit, replaying output` in stdout for the second run and zero task work in the second run's log.
  2. 02Edit one file listed in the new `inputs` (e.g., the root `tsconfig.base.json`) and re-run; the affected task should now show a miss while unrelated tasks still hit.
  3. 03Export a previously-undeclared env var to a new value and re-run; the task must miss until the var is added to `env`, then hit again on a repeat run with the same value.
  4. 04Inspect `turbo run <task> --dry-run=json` post-fix and confirm the emitted `inputs` set includes every config file and env var enumerated in the evidence step, with no extra files that the task does not read.
  5. 05Remove any temporary `cache: false` introduced during diagnosis; the task must remain correct without it.

Prevention and next steps

  • Adopt a repo convention that every `pipeline` task declares explicit `inputs`, `outputs`, `dependsOn`, and `env`; review these in PRs alongside `package.json` changes.
  • Run `turbo run <task> --dry-run=json` in CI on config-only PRs as a guardrail that the task's hash inputs still cover touched files.
  • Pin the Remote Cache backend to a single shared artifact store per environment (preview, production, CI) and document credentials scope so cross-environment misses are impossible by config.
  • Maintain a short living doc mapping each common tool (tsc, eslint, jest, vitest, next build, vite build) to the root config files and env vars it consults; update when tool versions change.

Safe commands and checks

npx turbo run <task> --dry-run=json
git status --porcelain
git rev-parse --abbrev-ref HEAD
npx turbo run <task> --dry-run=json | grep -A 50 '"task"'
cat turbo.json
cat package.json