Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Compare the Nx approach

Fixing Turborepo Remote Cache Misses

Your local machine gets instant cache hits, but every CI run reports cache miss, executing and rebuilds from scratch. Remote cache misses almost always come from a task hash that differs between environments: an environment variable that leaks into the build, a glob that captures a non-deterministic file, a lockfile mismatch, or a misconfigured TURBO_TOKEN/TURBO_TEAM. This page shows how to read the hash inputs with --dry=json and --summarize, then eliminate each source of drift.

Symptoms

• Packages in scope: web, ui, utils
• Running build in 3 packages
ui:build: cache miss, executing 7c9e1f2a3b4d5e6f
web:build: cache miss, executing a1b2c3d4e5f6a7b8

 Tasks:    3 successful, 3 total
Cached:    0 cached, 3 total
  Time:    1m48s
WARNING  failed to contact remote cache: 403 Forbidden
WARNING  Remote caching is disabled because no token was found.

The first block is the costly case: every task is a miss even though nothing meaningful changed. The second shows authentication failing outright, so Turborepo silently falls back to local-only caching.

Root cause

Turborepo computes a SHA-256 hash for every task from a precise set of inputs: the hashed contents of the task's input files, the resolved dependency set from the lockfile, the task's outputs declaration, the values of any environment variables it depends on, globalDependencies, and the turbo.json config itself. A remote cache hit requires that the hash computed on this machine matches a hash already stored in the remote cache. Any input that differs between your laptop and a CI runner produces a different hash and therefore a miss. Because Remote Caching Setup keys artifacts on that hash, an unstable input quietly defeats the entire cache. The most common culprits are environment variables that are present in CI but not locally, lockfile differences, and outputs that embed timestamps or absolute paths.

Diagnosing a remote cache miss A decision flow that checks token, then hash inputs, then output determinism to locate the cause of a cache miss. cache miss reported run --dry=json token / team set? no → fix auth hashes match? no → diff inputs deterministic outputs → hit
Work top to bottom: rule out auth, then input drift, then non-deterministic outputs.

Resolution

1. Confirm authentication and team

A 403 or "no token was found" means Turborepo never reached the remote cache. Set both the token and the team slug; the team must match the cache namespace:

export TURBO_TOKEN=your_cache_token
export TURBO_TEAM=your-team-slug
turbo run build --remote-only --summarize

--remote-only forces Turborepo to ignore the local .turbo cache so you can prove the remote layer works in isolation.

2. Dump the hash inputs

--dry=json prints exactly what went into each task hash without executing anything. Run it in both environments and diff the output:

turbo run build --dry=json > local-hashes.json
# On CI, capture the same and compare
turbo run build --dry=json > ci-hashes.json

Each task entry contains hash, inputs (file → content hash), hashOfExternalDependencies, and the resolved envMode and environment variable list. The first field that differs between the two files is your culprit.

3. Declare the environment variables a task depends on

Strict env mode (the default) means a task only sees the env vars it declares. If a build reads NODE_ENV or API_URL and you have not declared it, the value cannot change the hash on CI — but if the framework auto-detects it, the output changes while the hash does not, which corrupts the cache. Declare every meaningful variable:

{
  "globalEnv": ["NODE_ENV", "CI"],
  "tasks": {
    "build": {
      "env": ["API_URL", "NEXT_PUBLIC_*"],
      "inputs": ["$TURBO_DEFAULT$", "!**/*.test.ts"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    }
  }
}

4. Pin inputs, outputs, and global dependencies

A glob that matches a log file, a coverage report, or a .DS_Store will change the hash on every run. Scope inputs tightly and exclude generated files. List shared root files in globalDependencies so a change to them invalidates everything intentionally rather than randomly:

{
  "globalDependencies": ["tsconfig.base.json", ".env.production"],
  "tasks": {
    "build": {
      "inputs": ["src/**", "package.json", "tsconfig.json"]
    }
  }
}

5. Make outputs deterministic

If two builds of identical source produce byte-different output (embedded timestamps, absolute paths, randomized chunk hashes), the stored artifact is fine but downstream tasks that consume it will miss. Strip timestamps, set SOURCE_DATE_EPOCH, and avoid absolute paths in generated files. Ensure the lockfile is identical across environments, exactly as in Lockfile Management Strategies, because hashOfExternalDependencies is derived directly from it.

Validation

After applying fixes, prove the cache is shared end to end:

# Machine A: populate the remote cache
turbo run build --remote-only

# Machine B (or a clean CI runner): should replay, not rebuild
turbo run build --remote-only --summarize
cat .turbo/runs/*.json   # inspect cacheStatus for each task

A successful run reports cache hit, replaying logs and the summary's cacheStatus.timeSaved is non-zero.

CI guardrails

  • Set TURBO_TOKEN and TURBO_TEAM as CI secrets; verify with a --summarize step that fails the job if cacheStatus is all-miss on a no-op commit.
  • Commit turbo.json inputs/outputs/env declarations and review them like code; an undeclared env var is a latent cache bug.
  • Pin the package manager and lockfile so hashOfExternalDependencies is stable across runners.
  • Add a --dry=json artifact upload so a regression in hashing is debuggable from the CI logs.
  • Use a read-only cache token for untrusted fork PRs to prevent cache poisoning.

Frequently Asked Questions

Why do I get cache hits locally but misses in CI? The task hash differs between the two environments. The usual causes are an environment variable present in CI but not locally, a different lockfile resolution, or an input glob that captures a file CI generates. Run turbo run build --dry=json in both places and diff the inputs and env fields to find the first divergence.

What does "no cache hit" actually mean in Turborepo? It means Turborepo computed a task hash that does not exist in the cache it is reading, so it executes the task. It is not an error; it simply indicates the inputs to that task changed (or appear to have changed) since the last cached run.

How do I see exactly what went into a task hash? Use turbo run <task> --dry=json to print the resolved inputs, external dependency hash, and environment variables per task, and --summarize to write a per-run JSON summary under .turbo/runs/. Comparing these across machines pinpoints the unstable input.

Does a different environment variable always cause a miss? Only if the task declares that variable in env/globalEnv. The subtler failure is the reverse: an undeclared variable that changes the build output but not the hash, which stores a stale artifact under a hash that no longer matches reality. Declare every variable the build actually reads.

Related

Remote Caching Setup