Turborepo · intermediate

Turborepo CI-cache checklist

A practical debugging checklist for engineers when Turborepo's local cache and CI cache behave differently, covering cache key inputs, remote cache authentication, environment hashing, and verification steps to isolate whether divergence is caused by inputs, the cache layer, or environment drift.

The symptoms

  • Same task hash on a developer machine triggers a cache HIT locally but reports MISS in CI, even when the remote cache secret is configured and the daemon is enabled.
  • CI logs show "Remote Cache HIT" messages on some pipelines and "Remote Cache MISS" on others that share the same repository commit and Turborepo version.
  • Local `turbo run build` completes in seconds due to cache reuse, but the CI runner re-executes the task and uploads artifacts to the remote cache, causing longer wall-clock times.
  • The `turbo.json` schema check or task output globs change between local and CI runs, so the same inputs produce different cache keys.
  • Branch-prefixed cache namespaces appear empty in the remote cache dashboard even though `turbo login` succeeds and `turbo link` was run during CI setup.

Likely causes

  • Inputs to Turborepo's hash differ between local and CI: uncommitted files, `.env` contents, or `git status` indicating a dirty working tree are excluded locally via `.gitignore` but may be tracked differently when the runner performs a fresh checkout.
  • Remote cache credentials, team, or `TURBO_TEAM`/`TURBO_TOKEN` values differ across pipelines, so HITs resolve against distinct namespaces or the wrong account and silently fall through to local-only or remote-only cache.
  • `globalDependencies` and `globalEnv` entries in `turbo.json` do not cover environment variables the task actually reads, so the hash misses on CI where those variables exist locally.
  • CI runners use a different filesystem layout, package manager versions, or Node toolchain that the hash does not account for, producing divergent task outputs even when the hash matches.
  • The remote cache layer is writable from the local machine but read-only or unreachable from the CI network due to firewall, proxy, or region routing, so uploads succeed locally and reads fail remotely.

First ten minutes

  1. 01Run `turbo run build --summarize` locally and in CI; compare the `task-id` hash bytes and `inputs` list in the produced JSON to determine whether the hash is matching before the network is consulted.
  2. 02Run `turbo --info` (or set `TURBO_LOG_LEVEL=info`) and capture the `["reading remote cache"]` and `["remote cache"]` lines; record whether the response is HIT, MISS, or an auth/configuration error.
  3. 03Diff the effective `TURBO_TEAM`, `TURBO_TOKEN`, and `turbo.json` `remoteCache` block between the two environments using `turbo config` and a controlled echo of the secret's prefix only (never the secret value).
  4. 04Inspect `git status` and `git rev-parse HEAD` in both environments to confirm the commit and working-tree state used as hash inputs; a non-clean tree on CI plus a clean tree locally is the most common cause of local/CI hash divergence.
  5. 05Use `turbo run build --dry=json` to enumerate every environment variable Turborepo detected; cross-check that list against `globalEnv` and any `env` declaration inside the task.

Evidence to collect

  • The full JSON output from `turbo run <task> --summarize` for both local and CI runs, with identical inputs and command, so hash material can be diffed field by field.
  • CI runner logs filtered for the Turborepo prefixes `["reading remote cache"]`, `["remote cache"]`, `["cache miss, executing"]`, and `["cache bypass"]`, capturing HTTP-style status indicators emitted by the cache client.
  • The exact Turborepo version string (`turbo --version`) and the resolved `turbo.json` effective configuration, including resolved `globalDependencies`, `globalEnv`, per-task `env`, and `outputs` globs.
  • A controlled inventory of process environment variables in both environments, restricted to keys documented in `turbo.json`, with secret values redacted to prefix-only fingerprints.
  • Remote cache dashboard metrics for the affected team and branch within the window of the failing CI run, to confirm whether the artifact was uploaded, evicted, or never reached the cache.

Where to look

  • Boundary: hash-input boundary — the set of files, env vars, and config Turborepo folds into the task hash; divergence here means the cache key is correct but different per environment.
  • Boundary: cache-transport boundary — the network path between the runner and the remote cache service; auth, DNS, and proxy behavior control whether reads return cached artifacts or fall back to MISS.
  • Boundary: namespace/identity boundary — the `TURBO_TEAM` + `TURBO_TOKEN` pair, the linked repository, and the resolved `remoteCache` URL inside `turbo.json`.
  • Boundary: toolchain boundary — Node.js, package manager, and OS-level differences that affect task output but are not necessarily captured by `globalDependencies` or `globalEnv`.
  • Boundary: artifact/eviction boundary — the cache storage layer; entries can be uploaded, fetched, then evicted between runs, producing an apparent MISS without a hash change.

Diagnostic steps

  1. 01Step 1: Standardize the hash input. Run `turbo run <task> --summarize` in both environments with the same commit, a clean working tree, and an identical, recorded subset of environment variables; if hashes now match, the divergence was input-driven.
  2. 02Step 2: Confirm remote cache reachability. With credentials redacted, run `turbo run <task> --filter=... --log-prefix=remote --no-cache` after manually pre-warming one entry, then re-run without `--no-cache`; if the second run logs HIT, the transport boundary is healthy.
  3. 03Step 3: Audit environment coverage. Diff the dry-run JSON from both runs and confirm every variable the task actually reads is declared in `globalEnv` or per-task `env`; undeclared variables are common silent MISS contributors.
  4. 04Step 4: Verify namespace identity. Compare the effective `TURBO_TEAM`, the linked repository, and the configured `remoteCache.url`; mismatched teams or an unlinked repository cause reads to silently target a different (often empty) namespace.
  5. 05Step 5: Isolate toolchain drift. Pin Node.js, the package manager, and any compiler binaries used by the task, then re-run; if cache HITs now appear, toolchain-version drift is the residual cause.
  6. 06Step 6: Inspect the artifact store. Check the remote cache dashboard for the team/branch pair within the failure window to confirm whether the artifact is present, evicted, or absent; this distinguishes a hash miss from a storage miss.

Common mistakes

  • Assuming "clean cache" means "remote MISS is expected" without first verifying that the hash inputs match; a clean local cache can mask a hash mismatch by always producing a fresh write.
  • Hard-coding `TURBO_TOKEN` in CI secrets without scoping it to the correct team, so CI writes to one namespace and reads from another, producing consistent MISSes against otherwise identical commits.
  • Using `outputs` globs in `turbo.json` that differ between the developer's branch and `main`, so the cache key is technically valid but the artifact retrieved is for the wrong set of outputs.
  • Forgetting `globalEnv` for variables the task reads at runtime (e.g., `NODE_ENV`, `CI`, deployment flags), so the local hash omits variables that CI injects, and the hashes never align.
  • Diagnosing a remote-cache transport failure as a code change, when the logs actually show an authentication, DNS, or proxy error code from the cache client.

Safe fixes

  • Conditionally add undeclared but task-relevant environment variables to `globalEnv` in `turbo.json`, only after the dry-run JSON confirms Turborepo detected them at runtime; this aligns hashes without leaking sensitive values into the cache key derivation logic.
  • Conditionally standardize CI by ensuring the runner checks out a clean tree (`git status --porcelain` returns empty) before invoking Turborepo, so `.gitignore` and `.git`-tracked inputs hash the same way locally and remotely.
  • Conditionally re-link the repository with `turbo link` in CI setup and verify the resolved team against a known-good pipeline; this corrects namespace mismatches without changing the cache configuration itself.
  • Conditionally pin Node.js and the package manager via the CI configuration (e.g., `.nvmrc`, `packageManager` field) so the toolchain boundary produces matching task outputs; only deploy this fix after Step 5 confirms drift.
  • Conditionally add the remote cache host to the CI network allowlist after Step 2 evidence shows the transport is being blocked; do not bypass proxy or TLS settings without first capturing the exact client error line.

Prove the fix

  1. 01Run `turbo run <task> --summarize` on the same commit in both environments; the recorded `task-id` hash and the `inputs` list are byte-identical between local and CI for at least three consecutive runs.
  2. 02The CI log shows a `["remote cache"]` entry indicating HIT for the affected task on a clean run, with wall-clock time reduced to roughly the local cached duration and zero uploaded artifacts for that task.
  3. 03Removing the variable just added to `globalEnv` reintroduces the MISS locally; re-adding it restores the HIT, proving the variable was a hash input and not a coincidental change.
  4. 04The remote cache dashboard records both a write and a subsequent read of the artifact for the same task hash within the same pipeline window, confirming end-to-end cache reuse rather than a coerced write-only path.
  5. 05A subsequent commit that intentionally changes a hash input produces a single fresh write and a MISS in CI, demonstrating that the cache is responding to input changes rather than always returning MISS.

Prevention and next steps

  • Maintain a documented mapping between the variables any task reads at runtime and the matching `globalEnv` or per-task `env` entries in `turbo.json`, updated as part of the task's definition of done.
  • Treat `turbo run --summarize` output as a first-class CI artifact; archive it so future local/CI divergence can be diffed without re-running the failing pipeline.
  • Pin Node.js, the package manager, and the resolved Turborepo version in CI configuration, and run a periodic local/CI hash parity check on a no-op branch to catch silent drift early.
  • Restrict remote-cache tokens to the smallest team scope required, and re-run `turbo link` automatically in CI setup so the namespace identity cannot drift unnoticed between pipelines.

Safe commands and checks

git rev-parse HEAD && git status --porcelain
turbo --version
turbo run <task> --summarize
turbo run <task> --dry=json
turbo config
TURBO_LOG_LEVEL=info turbo run <task> 2>&1 | grep -E 'reading remote cache|remote cache|cache miss|cache bypass'
turbo run <task> --log-prefix=remote