use cache, cache tags, CDN or KV) has tenant-isolation rules of
its own and is out of scope here.
Cache layers
The Turbo cache is the only layer that replaces work. Every other layer only
saves downloads. Restoring an archive is not a task hit: Turbo still verifies
each task hash. Read
Cached: N cached, M total in the run output for actual
reuse.
How Turbo decides a hit
A task hash combines a global hash and a task hash.- Global hash:
.gitattributes, the root package’s external dependencies, the root package’s workspace dependency closure,globalEnvvalues,globalPassThroughEnvnames, and the rootturbo.jsondefinition. A change to any of these invalidates every task. - Task hash: the task’s resolved
inputs(default: git-tracked files in the package), its external dependency hash frombun.lock, itsenvvalues, and the hashes of every task itdependsOn.
type-check depends on
^build and ^type-check, and test depends on transit, which depends on
^transit. A source edit in packages/utils therefore invalidates every
dependent type check and test. Do not narrow these links. In particular, do
not exclude upstream __tests__ folders from them:
apps/web/src/app/api/v1/sso/routes.test.ts imports
packages/sso/src/__tests__/fixture by relative path.
Invariants
scripts/ci/turbo-cache-config.test.js enforces these rules:
- No root runtime workspace dependencies. A root
"@tuturuuu/email-service": "workspace:*"dependency put email-service, utils, types, internal-api and supabase into the global hash, so any edit to them invalidated every task. Root scripts that need a workspace package import its source path instead (see the Docker watcher inscripts/watch-blue-green/incident-email.js). The only allowed root workspace link is the@tuturuuu/typescript-configdev dependency, which legitimately affects every type check. - No volatile or machine-specific hashed env. Never hash run IDs, SHAs,
timestamps,
GITHUB_*/RUNNER_*, or machine paths (PUB_CACHE,LOCALAPPDATA,USERPROFILE). Put machine paths inglobalPassThroughEnv. Only deploy builds hashPLATFORM_BUILD_*, because the emitted app embeds that identity.globalEnvstill hashes runtime secrets such asOPENAI_API_KEYandSUPABASE_SECRET_KEYinto every task, so a shell that exports them gets no hits from CI. Keep them in app.envfiles, not the shell profile. Moving them to task env does not help today, because no-script placeholderbuildtasks would still carry them into every type-check hash. - No remote-cache credentials in any env list.
TURBO_TOKEN,TURBO_TEAM,TURBO_APIandTURBO_REMOTE_CACHE_SIGNATURE_KEYstay step-scoped (seeci-cache-policy.test.js). - Caching is opt-out only for an allowlist:
dev, persistentserve:*tasks,test:watch,test:e2e, and the runtime browser suite. Every other task declaresoutputs; check-only tasks use[]. - Narrowed
inputskeep$TURBO_DEFAULT$. Packageturbo.jsonfiles extend//and add to inherited inputs with$TURBO_EXTENDS$. - Gitignored files that change results are hashed.
testhashes.env*because Vitest configs callloadEnv(process.cwd()). Packages whose type check runswrangler typesorcf workers typeshash.dev.vars*and.env*, and cache the generated declarations their tsconfig reads (worker-configuration.d.tsor.cloudflare/types/**) so a cache hit on a fresh checkout restores them. - The local cache is bounded:
cacheMaxAge: "7d"andcacheMaxSize: "4GB". These settings do not change task hashes.
Measured effect
A dry-run replay (turbo run <task> --dry=json, no task executed) of 40
consecutive first-parent main commits on 2026-10-23 measured the share of
task hashes that already existed at the parent commit. That share is the hit
rate a perfectly warm cache seeded by the PR base would give.
The remaining full misses are dependency upgrades and edits to
turbo.json
env lists. Both are legitimate global changes. Most other misses are correct
cascades from shared packages.
The replay is an upper bound. Real CI only reaches it when a warm cache is
available. In the same week the repository’s Actions cache was at its 10 GB
limit. Pull-request-scoped Flutter SDK (1.6–2.1 GB each) and Gradle (3.6 GB)
entries evicted the default-branch Turbo fallback and Supabase image archives,
so branch type checks logged Cache not found and ran cold. Mobile toolchain
caches must restore on pull requests and save only from main, like the Bun
and Supabase caches already do.
Trust model
- Pull requests, forks and Dependabot get no Turbo remote-cache credentials. They restore only default-branch Actions caches and never save them, which prevents cache poisoning.
- Trusted
mainandproductionjobs read and write the remote cache. - The planned self-hosted cache adds signed artifacts
(
TURBO_REMOTE_CACHE_SIGNATURE_KEY) and read-only tokens for untrusted contexts. Its runbook covers cutover, rollback and cost. Until cutover, the Vercel-backed cache stays the fallback. - Local machines use remote credentials only from the user’s shell profile or
secret store. Never commit them or write them to
.envfiles that Turbo hashes.
Debug a miss
-
Compute hashes without running anything:
-
Repeat on the comparison commit, or in the CI environment, then compare
globalCacheInputs. IfhashOfInternalDependenciesdiffers, a root workspace dependency changed. IfenvironmentVariablesdiffers, a hashed env value differs between the two runs. -
For one task, compare
inputs(file to git hash),hashOfExternalDependencies,environmentVariables.configured, and the hashes of the tasks listed independencies. The first differing component is the cause. -
In CI, read the run logs.
cache miss, executing <hash>versuscache hit, replaying logs <hash>gives per-task results. The Turbo fallback archive step summary shows whether the archive restored at all. -
--summarizewrites.turbo/runs/<id>.jsonwith each task’scache.status,local/remotesource, andtimeSaved.
Run only what changed
Combine affected selection with a warm cache. Unchanged dependency tasks replay from the cache, and only the changed packages and their dependents execute.turbo.json, root package.json,
bun.lock root entries, .gitattributes) selects everything; that is
expected. For a single package, a package-local command such as
bun --cwd packages/ai vitest run <file> avoids Turbo entirely. Queue broad
runs with ttr resources run -- <command>.
Share the local cache across worktrees
Each worktree has its own.turbo/cache by default. Trusted machines can share
one bounded cache across worktrees by exporting an absolute path in the shell
profile:
cacheMaxAge
and cacheMaxSize bounds apply to the shared directory. Do not set
TURBO_CACHE_DIR in CI: the fallback archive action expects .turbo/cache.
Cost bounds
- GitHub Actions cache: 10 GB per repository with least-recently-used
eviction. Save only from
main, keep keys stable, and do not let pull-request refs create multi-GB entries. - Local Turbo cache: 7 days and 4 GB per cache directory.
- Remote cache: request, size, TTL and operation-count bounds for the self-hosted Worker are defined in its runbook.