Debugging Why a Turborepo Task Is Never Cached
A task always executes and never replays from cache, even on identical inputs. This page shows how to find why Turborepo refuses to cache a task and how to make it cacheable.
Exact symptoms and error messages
web:build: cache miss, executing 1a2b3c...
# run again, no changes
web:build: cache miss, executing 1a2b3c...
Root cause analysis
Turborepo will not cache a task that declares no outputs, is marked cache: false, is persistent, or writes outside its declared output globs. Without knowing what a task produces, Turborepo has nothing to store or restore, so it re-executes every time. Correct output declaration is core to Remote Caching Setup and the deterministic-output rules in Turborepo Pipeline Configuration.
Turborepo will not cache a task it cannot describe the outputs of. If a task declares no outputs, is explicitly cache: false, is marked persistent, or writes files outside its declared output globs, there is nothing for Turborepo to store or restore, so it re-executes every time. The fix is almost always to declare what the task produces accurately, so the cache has something concrete to save and replay.
A subtler cause is outputs written outside the declared globs. If build declares dist/** but also emits a manifest to .cache/, that file is invisible to the cache — restoring from cache produces an incomplete result, so in practice you disable caching to avoid the corruption, and the task never replays. Declaring every output path is what makes a cached restore complete and therefore trustworthy.
Turborepo caches a task by storing and restoring its declared outputs, so a task with no outputs has nothing to cache and re-executes every time. This is the single most common cause of a task that never caches: the pipeline defines the task's dependencies and command but omits the outputs glob, so Turborepo runs it, sees no declared artifact to store, and cannot replay it on a later run. The fix is to declare exactly the files the task produces.
The subtler causes are a task explicitly marked cache: false, a persistent task that never exits, or a task that writes outputs outside its declared globs. The last is insidious: if build declares dist/** but also emits a manifest to .cache/, restoring from cache produces an incomplete result, so in practice caching is disabled to avoid shipping a partial build — and the task appears to never cache. Every path the task writes must be either declared as an output or deliberately excluded.
Resolution and configuration patch
Declare the task's outputs (and remove any accidental cache: false):
// turbo.json
{
"tasks": {
"build": {
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"cache": true
}
}
}
Ensure the build writes only into declared paths; artifacts written elsewhere are invisible to the cache and defeat replay.
Declare every output the task produces, using negations for volatile subpaths, and reserve cache: false for genuinely non-deterministic work:
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": { "persistent": true, "cache": false }
}
}
After declaring outputs, verify with pnpm turbo run build --summarize that the task now reports a cache status and that the produced files fall inside the declared globs. Delete the outputs, restore from cache, and confirm the result is byte-complete.
CLI validation and debug commands
# See why a task is not cached
pnpm turbo run build --summarize
# Confirm outputs are declared and produced
pnpm turbo run build --dry=json | jq '.tasks[0].outputs'
Prevention and CI guardrails
- Declare
outputsfor every cacheable task; an empty list disables caching. - Reserve
cache: falsefor genuinely non-deterministic tasks only. - Keep
persistenttasks (dev servers) out of the cached build graph. - Verify with
--summarizethat produced files fall inside declared output globs.
- Declare
outputsfor every cacheable task; an empty list disables caching. - Use output negations to exclude volatile subpaths (like
.next/cache) from the cached artifact. - Mark long-running tasks
persistentso they stay out of the cached graph. - Reserve
cache: falsefor genuinely non-deterministic tasks, not as a blanket workaround.
Declaring outputs completely
The single most common reason a task never caches is an incomplete outputs list. Turborepo caches exactly the files you declare, so any artifact the task produces but does not list is lost on a cache restore — which makes caching unsafe and effectively forces you to turn it off.
// turbo.json
{
"tasks": {
"build": { "outputs": ["dist/**", ".next/**", "!.next/cache/**"] }
}
}
List every directory the build writes, and use negations to exclude volatile subpaths (like .next/cache) that should not be part of the cached artifact. The test is simple: delete the outputs, restore from cache, and confirm the result is byte-complete. If anything is missing, the outputs list is too narrow — widen it until a restore reproduces the full build, at which point the task caches correctly and replays instead of re-running.
Persistent and non-deterministic tasks
Some tasks genuinely should not cache, and recognizing them prevents wasted debugging. A dev server is persistent — it never exits, so it has no final output to store. A task that embeds a timestamp or a random value is non-deterministic, so a 'cached' result would be wrong. For these, disabling caching is correct, not a bug.
{
"tasks": {
"dev": { "persistent": true, "cache": false },
"build": { "outputs": ["dist/**"] }
}
}
Mark long-running tasks persistent so Turborepo keeps them out of the cached graph, and reserve cache: false for tasks whose output legitimately varies run to run. The goal is to cache everything deterministic and exclude only what truly cannot be — rather than blanket-disabling caching because one task misbehaves, which throws away the replay benefit for every well-behaved task alongside it.
Verifying outputs are complete
The test that a task's outputs are declared completely is simple and worth running whenever you add or change a cacheable task: delete the outputs, restore from cache, and confirm the result is byte-identical to a fresh build. If anything is missing after the restore, the outputs list is too narrow — a subpath the build writes was not declared, so it was not stored and is absent on replay. Widening the list until a restore reproduces the full build is what makes caching safe, because an incomplete cached artifact is worse than no cache: it silently ships a partial build.
The --summarize output is the diagnostic tool here, showing each task's hash, its declared outputs, and whether it hit or missed. Reading it tells you immediately whether a task is uncacheable because it has no outputs, because it is marked cache: false, or because its inputs are unstable. Making this verification part of adding a task — rather than discovering an incomplete cache in production — keeps the cache trustworthy, so that a hit is genuinely equivalent to a rerun and the speed-up caching promises is real rather than a source of subtle staleness.
Distinguishing uncacheable from misconfigured tasks
Some tasks legitimately should not cache, and recognizing them prevents wasted debugging effort chasing a cache hit that should never happen. A development server is persistent — it never exits, so it has no final output to store — and a task that embeds a timestamp or reads a live external service is non-deterministic, so a replayed result would be wrong. For these, the absence of caching is correct, and marking them persistent or cache: false documents that intent rather than leaving it looking like a bug.
The misconfigured case is different and fixable: a task that produces deterministic file outputs but never caches because those outputs are undeclared, or because it writes outside its declared globs. The way to tell them apart is to ask what the task produces. If it produces files that are a pure function of its inputs, it should cache and the fix is to declare the outputs; if it produces nothing storable or something non-deterministic, it should not cache and the configuration should say so. Getting this distinction right means caching accelerates every deterministic task while the genuinely uncacheable ones are excluded deliberately, rather than blanket-disabling caching because one task misbehaves.
Reading the --summarize output to diagnose caching
When a task is not caching as expected, the fastest path to the cause is Turborepo's summary output rather than guessing at the configuration. turbo run build --summarize writes a JSON summary for each task showing its computed hash, the inputs that fed it, its declared outputs, and whether it hit or missed the cache. Reading it answers the diagnostic questions directly: a task with an empty outputs array is uncacheable because it has nothing to store; a task whose hash changes between otherwise-identical runs has an unstable input; and a task marked cache: false shows as deliberately excluded.
The summary is also how you catch the subtle case of outputs written outside the declared globs. If the summary shows dist/** as the declared output but the build also writes to .cache/, restoring from cache reproduces only dist/** and the result is incomplete — which is why the task appears to never cache safely. Comparing the declared outputs against what the build actually produces, using the summary as the reference, is what surfaces this mismatch. Making --summarize the first step when a task misbehaves turns caching from an opaque black box into an inspectable system where every hit, miss, and exclusion has a visible, explainable cause.
Frequently Asked Questions
Why does a task with no outputs still not cache?
Turborepo caches a task's declared outputs; with none declared it has nothing to store, so it cannot replay and re-runs every time. Declare the files the task produces.
Should dev servers be cached?
No — mark long-running tasks persistent: true and keep them out of the cached build graph. Caching only makes sense for tasks that produce deterministic file outputs and then exit.
Why does a task with no outputs never cache?
Turborepo caches a task's declared outputs; with none declared it has nothing to store, so it cannot replay and re-runs every time. Declare every directory the task produces.
How do I know my outputs list is complete?
Delete the outputs, restore from cache, and check the result is byte-complete. Anything missing means the list is too narrow — widen it (with negations for volatile subpaths) until a restore reproduces the full build.
Should a dev server be cached?
No. Mark it persistent: true so Turborepo keeps it out of the cached graph. Caching only applies to tasks that produce deterministic file outputs and then exit.
Why does my Turborepo task run every time even with no code changes?
Almost always because it declares no outputs, so Turborepo has nothing to store and cannot replay it. Declare the files the task produces (with negations for volatile subpaths), then verify with --summarize that it reports a cache status.
How do I know my outputs list is complete?
Delete the outputs, restore from cache, and check the result is byte-identical to a fresh build. Anything missing means the list is too narrow — a subpath the build writes wasn't declared. Widen it until a restore reproduces the full build.
Should a dev server or a task with a timestamp be cached?
No. Mark a dev server persistent (it never exits, so has no output to store) and a non-deterministic task cache: false (a replayed result would be wrong). Caching applies only to tasks that produce deterministic file outputs and then exit.
How do I diagnose why a Turborepo task isn't caching?
Run turbo run <task> --summarize and read the per-task JSON: an empty outputs array means it's uncacheable, a hash that changes between identical runs means an unstable input, and cache: false means it's deliberately excluded. The summary makes every hit, miss, and exclusion inspectable.
Why does a task cache locally but not in CI?
A hashed input differs between the two — commonly an undeclared env var or an unpinned tool version. Diff the --summarize hash inputs between a local and a CI run to find the divergent input, then declare it so both hash identically.
Does marking a task cache: false speed anything up?
No — it forces the task to always execute, which is correct only for genuinely non-deterministic work. For a deterministic task, cache: false throws away the replay benefit; declare its outputs instead so it can be cached and skipped when unchanged.
Related
- Remote Caching Setup — the shared remote cache configuration this fix builds on.