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.
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_TOKENandTURBO_TEAMas CI secrets; verify with a--summarizestep that fails the job ifcacheStatusis all-miss on a no-op commit. - Commit
turbo.jsoninputs/outputs/envdeclarations and review them like code; an undeclared env var is a latent cache bug. - Pin the package manager and lockfile so
hashOfExternalDependenciesis stable across runners. - Add a
--dry=jsonartifact 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 — the cache architecture and token model that hashes are stored against.
- Self-Hosting a Turborepo Remote Cache — run your own cache server when debugging hosted-cache auth.
- Turborepo Pipeline Configuration — declaring
inputs,outputs, anddependsOnthat feed the hash. - Lockfile Management Strategies — keeping
hashOfExternalDependenciesstable across machines.