What this usually means
Turborepo's caching is deterministic: it hashes task inputs (source files, environment variables, dependencies) and compares them. A cache miss means the hash changed. Common causes include: implicit environment variables (e.g., `CI`, `GITHUB_REF`), different file modification times on network filesystems, missing `turbo.json` configuration for inputs, or incorrect remote cache credentials. The non-obvious ones are environment variables injected by CI systems that aren't declared as `dependsOn` or `env` in your pipeline configuration.
The first ten minutes — establish facts before touching code.
- 1Run `turbo run build --dry --graph` to see the task graph and which tasks have cache hits/misses
- 2Compare hashes: `turbo run build --dry` shows `hash=...`; share the hash with a teammate on the same commit to see if they match
- 3Check input file hashing: `turbo run build --dry=json | jq '.tasks[] | select(.task=="build") | .hash,.inputs'`
- 4List all environment variables in the task scope: `turbo run build --dry=json | jq '.tasks[].environment'`
The specific files, logs, configs, and dashboards that usually own this bug.
- search`turbo.json` – check `inputs`, `outputs`, `dependsOn` and `env` arrays
- searchCI pipeline config (e.g., `.github/workflows/*.yml`) – look for `env` or `secrets` that might alter the hash
- search`package.json` scripts – ensure build scripts don't include random or date-based tokens
- search`node_modules/.cache/turbo/*.log` – turbo debug logs (set `TURBO_LOG_LEVEL=debug`)
- searchRemote cache server (e.g., Vercel Remote Caching dashboard) – verify cache entries exist for the hash
Practical causes, not theory. These are the things you will actually find.
- warningUndeclared environment variables in `turbo.json` (e.g., `CI=true`, `GITHUB_REF`) that change the hash
- warningFile modification times (mtime) differing across machines due to `git clone` vs `git checkout` on network drives
- warningInconsistent `.gitignore` or missing files causing different input sets
- warningRemote cache misconfiguration: wrong team ID, API URL, or token (e.g., `TURBO_TEAM`, `TURBO_TOKEN`, `TURBO_API`)
- warningBuild scripts that generate unique files (e.g., `BUILD_ID`, timestamps) inside the output directory
- warningUsing `--no-cache` or `--force` inadvertently in CI scripts
Concrete fix directions. Pick the one that matches your root cause.
- buildExplicitly declare all environment variables in `turbo.json` under `env` or `dependsOn` – even CI-specific ones like `CI`, `GITHUB_REF`
- buildUse `inputs` to limit the files that affect the cache key: specify globs for source files only, exclude generated files
- buildNormalize timestamps: set `git config core.fileMode false` and use `--cached` for reproducible checkouts in CI
- buildFor remote cache, verify connectivity: `curl -I "$TURBO_API/v8/artifacts/..."` and check token scopes
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `turbo run build --dry` twice on the same commit without changes – should show `cache hit` on second run
- verifiedCompare hashes from two identical CI runs: the hash output should be identical
- verifiedAdd `--summarize` flag (v1.8+) to generate a summary file and inspect `cacheSummary`
- verifiedCheck remote cache by fetching artifact: `curl -H "Authorization: Bearer $TURBO_TOKEN" "$TURBO_API/v8/artifacts/<hash>"` should return 200
Things that make this bug worse or harder to find.
- warningAdding `--no-cache` to disable cache entirely instead of fixing the input hash
- warningIgnoring environment variables: even `TZ=UTC` or `LANG` can change the hash if not declared
- warningUsing `outputs` that are too broad (e.g., `**/*`) – include generated files that change per build
- warningAssuming local cache is enabled by default – check `turbo.json` for `cache: false` on individual tasks
CI builds never hit remote cache despite identical code
Timeline
- 09:15Deploy to staging triggers monorepo build in CI
- 09:17Build shows `remote cache miss` for all 12 tasks
- 09:20Check `turbo run build --dry` on local with same commit: hash differs from CI
- 09:25Compare environment variables: CI has `CI=true`, `GITHUB_REF=refs/heads/main`
- 09:30Add `"env": ["CI", "GITHUB_REF"]` to `turbo.json` for all tasks
- 09:32Rerun CI: still cache miss – hashes now match but remote cache doesn't have the new hash
- 09:40Check remote cache API: `curl -I` returns 404 for the hash; cache was never populated because earlier builds had different env vars
- 09:45Force a build to populate cache (first build after fix)
- 09:47Second CI run: all tasks hit remote cache. Build time drops from 14 min to 2 min.
We had been noticing CI build times creeping up. The team was using Turborepo with Vercel Remote Caching, but every pull request took 12-15 minutes. I ran `turbo run build --dry` locally and saw cache hits. In CI, everything was a miss. The first thing I checked was the hash. I printed the hash from a local build and compared it to the CI hash from the same commit. They were different.
I started diffing the environment variables. CI had `CI=true`, `GITHUB_REF`, and `GITHUB_SHA` set. Our `turbo.json` didn't declare any `env` keys, so those variables were not part of the hash computation locally – but they were in CI, causing the hash to change. I added them to the `env` array for all tasks. That made hashes match.
But the remote cache still didn't hit. The problem was that the cache had never been populated with the new hash because all previous builds had a different env set. The first build after the fix would be a miss, but it would upload the artifact. Subsequent builds then hit. We also added a CI step to force a cache upload on the first run after a config change. Lesson: env vars must be declared, and cache population requires a seed run.
Root cause
Undeclared CI environment variables (`CI`, `GITHUB_REF`) caused different cache hashes between local and CI environments.
The fix
Declared all relevant environment variables in `turbo.json` under `env` for each task, and ran a seed build to populate the remote cache.
The lesson
Always explicitly list environment variables that affect your build in `turbo.json`'s `env` or `dependsOn`. Use `--dry` to compare hashes across environments.
Turborepo hashes a task's inputs to create a cache key. The default inputs are all files in the package's directory and its dependencies' directories, plus environment variables listed in `env` or `dependsOn`. The hash is SHA256 of a sorted JSON blob containing file paths with their hashes, and environment variable key-value pairs.
A common mistake: assuming that only source files matter. But any environment variable that is read during the task execution changes the hash. For example, `CI=true` or `NODE_ENV=production` will produce a different hash than without them. To see what's included, run `turbo run build --dry=json | jq '.tasks[0].environment'`.
Start with `turbo run build --dry`. This prints the hash for each task and whether it's a hit or miss. If a task shows `cache miss`, note the hash. Then run the same command again with no changes – it should hit. If it still misses, something about the environment is non-deterministic.
Check file modification times: on some filesystems (e.g., NFS, WSL), `mtime` can be inconsistent. Turborepo uses file content hashes by default, but if you have `inputs` that include files with non-deterministic content (like logs or generated files), the hash changes. Run `turbo run build --dry=json | jq '.tasks[].inputs'` to see the list of files included.
If local cache works but remote doesn't, first verify connectivity: `curl -I "$TURBO_API/v8/artifacts" -H "Authorization: Bearer $TURBO_TOKEN"`. A 401 means token is wrong; a 404 means no artifact for that hash (expected for first run).
Check that `TURBO_TEAM` and `TURBO_API` are set correctly. For Vercel Remote Caching, the API is `https://api.vercel.com/v8/artifacts` and team is your Vercel team ID. Use `turbo login` locally to test – it sets these variables automatically. In CI, you must pass them explicitly.
Always declare environment variables that affect the build output in `turbo.json`. Use the `env` key for variables that are read directly, and `dependsOn` for variables that affect task ordering or input. For example: `"build": { "dependsOn": ["^build"], "outputs": ["dist/**"], "env": ["CI", "GITHUB_REF", "NODE_ENV"] }`.
Be careful with wildcards: if you declare `"env": ["*"],` it includes all environment variables, which makes caching useless. Instead, list only the ones that matter. To find which env vars are being used, you can run your build with `TURBO_LOG_LEVEL=debug` and look for `env vars` in the logs.
CI platforms inject many variables. Not all of them affect your build, but Turborepo doesn't know that. The safest approach is to explicitly list them. Common ones: `CI`, `GITHUB_REF`, `GITHUB_SHA`, `GITHUB_RUN_ID`, `GIT_COMMIT`, `BUILD_NUMBER`. If you use a variable in your build script (e.g., `process.env.GITHUB_REF`), it must be declared.
A trick: in your CI config, add a step that runs `turbo run build --dry=json | jq '.tasks[].environment'` to see exactly which env vars are being included. Then copy those into your `turbo.json`. This ensures parity.
Frequently asked questions
Why does a cache miss happen on the first run after a git pull?
Git operations can change file modification times even if content is unchanged. Turborepo v1.7+ uses file content hashing by default, but if you have custom `inputs` that rely on `mtime` (e.g., if you set `"inputs": ["src/**/*.ts"]` without content hash), you'll get misses. Use the default or add `"**/*.ts"` which uses content hash. Also, ensure `git checkout` doesn't change file permissions (use `git config core.fileMode false`).
How do I force a cache miss for a specific task?
You can't force a miss directly, but you can change an input. For example, set an environment variable `TURBO_FORCE_MISS=true` and declare it in `env` for that task. Alternatively, use `--force` on the entire pipeline: `turbo run build --force`. This bypasses cache for all tasks. To invalidate cache for a specific task, change its `inputs` or `env` in `turbo.json`.
What does `turbo run --dry` output actually mean?
`--dry` shows whether each task would be executed. A `cache hit` means the task is skipped and outputs restored from cache. A `cache miss` means the task will run. The `hash` is the cache key. You can compare hashes across machines to see if inputs differ. Use `--dry=json` for machine-readable output with full details on inputs and environment.
Can I use `dependsOn` with environment variables?
Yes, `dependsOn` can include environment variables in the form `$VAR_NAME`. For example, `"dependsOn": ["^build", "$NODE_ENV"]` makes the task depend on the `NODE_ENV` variable. This is equivalent to adding it to `env` but also affects task ordering. Use `env` for variables that don't affect ordering, and `dependsOn` for those that do.
Why does my remote cache work locally but not in CI?
This is almost always a credential or environment variable mismatch. Check that `TURBO_TOKEN`, `TURBO_TEAM`, and `TURBO_API` are set correctly in CI. Also ensure that the CI environment doesn't have extra variables that change the hash (e.g., `CI=true`). Use `--dry` to compare hashes between local and CI. The fix: declare all env vars in `turbo.json` and verify remote cache URL.