Turborepo · beginner
Turborepo build is slow despite a cache: identify the uncached task boundary
Turborepo builds that ignore cache are almost always bounded by a single uncached task graph boundary. This guide shows how to identify which task is missing a cache hit, why its inputs/outputs are untracked, and how to prove the fix with a HIT/MISS trace.
The symptoms
- •Total turbo run duration stays roughly the same on repeated runs even when CI shows cache restoration lines for other tasks.
- •Turbo reports a full HIT for sibling packages but the slowest task logs MISS or 'cache bypass' on every invocation.
- •Wall-clock time of the build pipeline is dominated by one task, while task count and source diff are small.
- •Local turbo run --summarize output shows one task with 'cache': false in the task summary JSON, while neighbors show 'cache': true.
Likely causes
- •The slow task's pipeline definition omits outputs, so Turbo cannot store or restore a tarball for it.
- •Inputs glob for the task includes volatile files such as lockfiles, env files, or generated directories that change every run.
- •dependsOn references a downstream task whose outputs are not declared, breaking the cache key chain.
- •A global dependency like an environment variable or globalFile is changing the hash without a corresponding code change.
- •The task is configured with 'cache': false or 'outputs' is set to an empty array in turbo.json for that pipeline entry.
First ten minutes
- 01Confirm the symptom by running the same turbo command twice with --summarize and comparing the duration waterfall; the unchanged task is your suspect boundary.
- 02Inspect turbo.json for the slow task and verify that 'outputs' is a non-empty array of path globs that match the artifacts the task actually writes.
- 03Diff the task's 'inputs' against the files it actually reads; mark any generated, lock, or env files that are not part of the source identity.
- 04Capture the per-task log lines and look for MISS, 'cache bypass', or 'cache miss, outputs are invalid' markers to confirm the uncached boundary.
- 05Check whether the slow task lists 'env' or 'globalEnv' entries that the shell actually exports, since each declared key widens the hash.
Evidence to collect
- •One turbo run with --summarize=/path/to/summary.json so the per-task timing and cache decision are persisted.
- •Raw log output of the slow task showing the exact cache decision string emitted by Turbo.
- •The current turbo.json pipeline entry for the slow task, including pipeline, outputs, inputs, dependsOn, env, and globalEnv.
- •Hash inputs for the task: the resolved file list and any non-file global deps that contributed to the hash.
- •A second run of the same command with no source change, to confirm whether the task is reproducible or has unstable inputs.
Where to look
- •turbo.json at the repository root, focusing on the pipeline entry whose task name matches the slow step.
- •The package.json scripts field of the package that owns the slow task, to confirm the exact command string Turbo executes.
- •The output directory the task writes to on disk, to verify that the path glob in 'outputs' actually matches real artifacts.
- •Workspace packages listed in dependsOn; their own pipeline entries must also declare outputs for the cache key chain to hold.
- •CI log section for the slow task, where cache decision strings are emitted before the command body runs.
Diagnostic steps
- 01Run the pipeline with --summarize and grep the summary JSON for the slow task name; record 'cache', 'duration', and 'hash' fields.
- 02From the summary, isolate the task whose 'cache' value is false on a clean repo with no source changes; that is the boundary.
- 03Hash the task inputs manually by running turbo run <task> --dry=json and inspecting the resolved dependencies and globalDependencies lists.
- 04Compare the dry-run hash of the slow task against the same task on a previous green run; a difference confirms the input drift.
- 05Re-run the task standalone with no extra flags and observe whether the cache decision flips to HIT; if it does, the boundary is a dep, not the task itself.
- 06Validate the 'outputs' glob by checking which files exist after the task finishes; any file written outside the declared glob is invisible to the cache.
Common mistakes
- •Assuming a cache hit on a sibling task means the whole pipeline is cached; per-task cache decisions are independent.
- •Adding 'src/**' to inputs without excluding generated files, causing the hash to change whenever the build emits a file under src.
- •Setting 'cache': false globally on a pipeline to silence a flapping task instead of fixing the unstable inputs.
- •Declaring outputs as a relative path that does not match the task's actual working directory or the package's root.
- •Forgetting that dependsOn on a workspace package uses that package's pipeline outputs, not its filesystem tree, for the cache key.
Safe fixes
- •If 'outputs' is empty or missing for the slow task, add the exact path glob that the task produces, validated against the on-disk artifacts after one run.
- •If inputs include volatile files, restrict 'inputs' to source-only globs and move lockfile or env-file tracking to globalDependencies or env.
- •If env or globalEnv is overdeclared, narrow it to the precise variables the task reads, so each declared key only widens the hash when it actually changes.
- •If a depended-on workspace task has no outputs declared, add outputs at that boundary so the upstream hash is stable.
- •After any change, re-run with --summarize twice in a row; the slow task should report 'cache': true on the second run with no source diff.
Prove the fix
- 01Second turbo run with no code change shows the previously slow task with 'cache': true in the summary JSON and a near-zero duration entry.
- 02turbo run <task> --dry=json returns an identical hash for the task across two consecutive invocations against the same working tree.
- 03Cache decision logs for the task show 'cache hit' on the second run, and the output tarball path is materialized from cache rather than re-written by the task body.
- 04End-to-end pipeline duration on the second run drops by approximately the duration of the previously uncached task, holding all other variables constant.
Prevention and next steps
- •Adopt a rule that every task in turbo.json must declare outputs, and review that rule in code review for any new pipeline entry.
- •Keep env and globalEnv entries minimal and audited; treat each declared key as a hash amplifier that must be justified.
- •Run --summarize in CI on a periodic job so a regression in cache hit rates surfaces before it is blamed on the build itself.
- •Treat 'cache': false as a temporary quarantine with a tracking ticket, not a permanent configuration, to keep uncached boundaries visible.
Safe commands and checks
turbo run <task> --summarize=/path/to/summary.json turbo run <task> --dry=json turbo run <task> --filter=<package-name> --summarize=/path/to/summary.json turbo run <task> --force cat /path/to/summary.json grep -E 'cache (hit|miss|bypass)' /path/to/task.log