Remote Caching and Distributed Build Coordination
Remote caching turns a monorepo task — build, test, lint, type-check — into a content-addressed lookup: Turborepo hashes every input that can affect a task’s output, checks whether an artifact for that hash already exists in a shared store, and replays the cached output instead of re-running the task. The payoff is that a build computed once on a developer’s laptop is reused verbatim by CI and by every teammate, so a clean-clone CI run that would take eight minutes collapses to a forty-second restore. This guide covers how Turborepo computes those hashes, the wire protocol it speaks to a cache server, how to wire up Vercel Remote Cache and a self-hosted alternative, the turbo.json pipeline that defines task inputs and dependencies, and how to debug the inevitable “why did this miss?” investigations. For the incremental-compilation model that complements task-level caching, see esbuild & Turbopack Workflows before treating the remote cache as a black box — a cache you cannot explain is a cache you cannot trust.
Prerequisites
Remote caching is a Turborepo feature; Turbopack consumes the incremental graph at the framework level, while Turborepo orchestrates and caches whole tasks across the workspace.
- Turborepo 2.x (
turbo@^2). The 1.x line works, but 2.x renamed the top-levelpipelinekey totasksinturbo.json, tightened env-var handling, and stabilized--remote-only/--summarizeoutput. This guide targets 2.x and calls out 1.x differences. - A package manager with workspaces: pnpm 8/9, npm 9+, or Yarn 3+. The lockfile is a hash input, so a clean, committed lockfile is mandatory.
- Node.js 18, 20, or 22. Turborepo’s binary runs across all three; the Node version itself is not in the hash by default, which is a deliberate gotcha covered below.
- A remote cache backend: either a Vercel account (zero-infra, the default) or a self-hosted server implementing the cache HTTP API.
If you have not yet established the per-package build/test scripts and workspace layout, do that first — remote caching amplifies a well-structured task graph and faithfully caches a broken one. For the framework-side compilation cache that pairs with task caching, review Turbopack Incremental Compilation.
Core Mechanics: Input Hashing and the Artifact Protocol
What goes into a cache key
For every task Turborepo runs, it computes a single content hash from a deterministic set of inputs. The defaults are: all non-gitignored files in the package directory, the package’s resolved dependency closure (read from the lockfile, not package.json ranges), the task command string, the contents of turbo.json, any globalDependencies, the values of every environment variable the task is declared to depend on, and — critically — the hashes of all upstream tasks pulled in via dependsOn. A ^build entry means “this task depends on the build of every internal package dependency,” so the hash transitively folds in the entire upstream subgraph. Change one byte of a shared ui package and every downstream web#build key changes with it.
The corollary is the source of nearly every confusing miss: anything that affects output but is not in the hash leads to a stale hit, and anything that varies between machines but is in the hash leads to a spurious miss. Environment variables are the usual culprit on both sides. In Turborepo 2.x, env vars referenced in code are not auto-included; you declare them in turbo.json under env (per task) or globalEnv (workspace-wide), and turbo warns about variables it sees used but not declared via its “strict” env mode.
The cache artifact and its protocol
A cache artifact is a gzip-compressed tarball of the task’s declared outputs (e.g. dist/**, .next/**) plus a small metadata file recording the captured stdout/stderr, so a cache hit can replay the original log output, exit code, and files. The remote cache speaks a small HTTP API the open-source community has standardized:
GET /v8/artifacts/{hash}?teamId=...— returns the tarball (200) or 404 on a miss.PUT /v8/artifacts/{hash}?teamId=...— uploads the tarball after a local task runs.POST /v8/artifacts/events— records hit/miss telemetry (best-effort).- Optional
x-artifact-tagheader carries an HMAC-SHA256 signature when artifact signing is enabled, so a consumer can verify the artifact was produced by a trusted machine before trusting its bytes.
Because the protocol is just authenticated GET/PUT of opaque tarballs keyed by hash, any server that implements it — Vercel’s hosted cache or a self-hosted one — is interchangeable from turbo’s point of view.
Configuration & CLI Reference
Complete turbo.json
// turbo.json — Turborepo 2.x. Lives at the monorepo root.
{
"$schema": "https://turborepo.com/schema.json",
// Files outside any package whose change should bust every task's hash.
"globalDependencies": ["tsconfig.base.json", ".env"],
// Env vars that affect every task's output (folded into all hashes).
"globalEnv": ["NODE_ENV"],
// Pass-through env vars that should NOT affect hashing (e.g. tokens).
"globalPassThroughEnv": ["TURBO_TOKEN", "TURBO_TEAM", "CI"],
"tasks": {
"build": {
// ^build = build all internal dependencies first; their hashes
// become inputs to this task's hash.
"dependsOn": ["^build"],
// Declared cache inputs; defaults to all package files if omitted.
// Narrowing inputs reduces spurious misses from unrelated edits.
"inputs": ["src/**", "package.json", "tsconfig.json"],
// What gets captured into the cache artifact on success.
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
// Env vars that legitimately change build output.
"env": ["NEXT_PUBLIC_API_URL", "SENTRY_RELEASE"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**", "test/**", "vitest.config.ts"],
// No outputs to cache, but logs/exit code are still replayed on a hit.
"outputs": []
},
"lint": {
"inputs": ["src/**", ".eslintrc.cjs"],
"outputs": []
},
"dev": {
// Long-running task: never cache, never batch.
"cache": false,
"persistent": true
}
}
}
Connecting to Vercel Remote Cache
# Turborepo 2.x. Run from the monorepo root.
# 1. Authenticate the machine against Vercel.
npx turbo login
# 2. Link this repo to a Vercel scope/team's remote cache.
npx turbo link
# 3. From now on, runs read/write the remote cache automatically.
npx turbo run build
CI without an interactive login
# In CI there is no browser for `turbo login`. Use a token instead.
# Set these as CI secrets, not in turbo.json.
export TURBO_TOKEN="<vercel access token>"
export TURBO_TEAM="<your-team-slug>"
# Optional: a custom API for a self-hosted cache (see below).
# export TURBO_API="https://cache.internal.example.com"
# turbo reads TURBO_TOKEN/TURBO_TEAM and uses the remote cache headlessly.
npx turbo run build test lint
Self-hosted cache server
Any server implementing the artifact API works. A minimal env-only setup points turbo at it:
# Turborepo 2.x against a self-hosted cache (e.g. the open-source
# turborepo-remote-cache server backed by S3 or local disk).
export TURBO_API="https://cache.internal.example.com"
export TURBO_TOKEN="<shared bearer token the server validates>"
export TURBO_TEAM="team_acme" # becomes the ?teamId= query param
# Optional: sign artifacts so consumers verify provenance with this key.
export TURBO_REMOTE_CACHE_SIGNATURE_KEY="<32+ byte hmac secret>"
npx turbo run build --remote-only # bypass local cache to prove remote works
To require signature verification, enable it in turbo.json:
// turbo.json — adds artifact signing on top of the config above.
{
"$schema": "https://turborepo.com/schema.json",
"remoteCache": {
// Reject any downloaded artifact whose HMAC tag fails verification
// against TURBO_REMOTE_CACHE_SIGNATURE_KEY.
"signature": true
},
"tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] } }
}
Numbered Workflow: Stand Up a Shared Remote Cache
- Write
turbo.jsonwith explicitoutputsandenv. A task with nooutputscaches only logs; a task that reads an undeclared env var produces stale hits. Declare both. Verify the file parses:npx turbo run build --dry=json | jq '.tasks[0].hashOfExternalDependencies'. - Prove local caching works first. Run
npx turbo run buildtwice. The second run must print>>> FULL TURBO(every task a cache hit) in well under a second. If it does not, remote caching will not help — fix the local determinism before going remote. - Connect the remote cache. Run
npx turbo loginthennpx turbo linkfor Vercel, or exportTURBO_API/TURBO_TOKEN/TURBO_TEAMfor a self-hosted server. - Seed the cache from a clean machine. Run
npx turbo run build --remote-onlyso artifacts are forced through the remote backend (local cache disabled), populating the store. - Wire CI to share the cache. Set
TURBO_TOKENandTURBO_TEAMas CI secrets and runnpx turbo run build test lint. The concrete GitHub Actions wiring is detailed in Configuring Remote Cache with Turborepo and Vercel. - Verify cross-machine hits. Clear the local cache (
rm -rf node_modules/.cache/turbo), then runnpx turbo run build --summarize. Open the generated.turbo/runs/*.jsonsummary and confirm tasks show"cache": { "status": "HIT", "source": "REMOTE" }.
Debugging & Failure Modes
Cache misses from undeclared environment variables
Symptom: identical source produces a miss on CI but a hit locally, or vice versa. Root cause: a build-affecting env var (NEXT_PUBLIC_*, a feature flag, an API URL) is not listed under the task’s env, so the two machines hash the same key but produce different output — or it is listed and differs between machines, flipping the key. Diagnose by diffing hash inputs across the two runs: npx turbo run build --dry=json > a.json on each machine and compare the tasks[].environmentVariables arrays. Fix by declaring every output-affecting variable under env/globalEnv and moving non-affecting tokens to globalPassThroughEnv.
Non-deterministic outputs
Symptom: the same inputs produce a cache hit, but the restored artifact misbehaves, or two clean builds of the same commit upload different artifacts. Root cause: the task embeds a timestamp, absolute path, Date.now(), or unsorted directory listing into its output. Turborepo trusts that a given hash maps to one canonical output; non-determinism silently breaks that contract. Diagnose by building twice into separate directories and diff -r-ing them. Fix the source of nondeterminism (pin timestamps, sort globs, strip absolute paths) before relying on the cache.
Inputs too broad — everything misses on every edit
Symptom: editing a README or a test file busts the build cache. Root cause: inputs is unset, so the default includes every package file. Fix by setting a tight inputs glob per task (src/**, package.json, tsconfig.json for build) so unrelated edits do not flip the key.
Remote unreachable or auth failures
Symptom: turbo logs Failed to fetch from remote cache or silently falls back to local-only. Root cause: bad TURBO_TOKEN, wrong TURBO_TEAM slug, or TURBO_API pointing at an unreachable host. Diagnose with npx turbo run build --remote-only -vv to surface the HTTP status. A 403 is a token/team mismatch; a connection error is a network or TURBO_API problem.
Stale hits after a toolchain upgrade
Symptom: a Node or compiler upgrade changes output, but turbo replays the old artifact. Root cause: the Node version is not a hash input by default. Fix by adding the toolchain to the hash explicitly — list the Node version via globalEnv (e.g. a NODE_VERSION you set) or include a .nvmrc/.node-version file in globalDependencies.
Performance Impact & Measurement
The headline metric is the share of tasks served from cache. Capture it with --summarize and read the run summary:
# Turborepo 2.x — emit a machine-readable summary of every task's cache status.
npx turbo run build test lint --summarize
# Then inspect cache sources:
jq '.tasks[] | {task: .taskId, status: .cache.status, source: .cache.source}' \
.turbo/runs/*.json
On a typical monorepo, a fully-warmed remote cache turns a multi-minute CI build+test into a sub-minute restore, because the dominant cost shifts from CPU to network download of a few tens of megabytes of tarballs. The two numbers worth tracking over time are the remote hit rate (target > 80% on PR branches that touch few packages) and artifact transfer time (if downloads dominate, the backend or its region is the bottleneck, not turbo). Use --remote-only to isolate remote-cache behavior from a warm local cache when measuring. A hit rate that collapses after a refactor almost always means a widely-shared package changed and legitimately invalidated its dependents — that is the cache working, not failing.
Compatibility Matrix
| Component | Versions | Remote cache support | Notes |
|---|---|---|---|
| Turborepo | 2.x | Full; tasks key, --remote-only, --summarize, signing |
Recommended baseline for this guide. |
| Turborepo | 1.x | Full, but uses pipeline key and older env handling |
pipeline → tasks rename is the main migration step. |
| Vercel Remote Cache | hosted | Native (turbo login / turbo link) |
Zero infrastructure; TURBO_TOKEN/TURBO_TEAM in CI. |
| Self-hosted cache | artifact API v8 | Full via TURBO_API/TURBO_TOKEN |
Any server implementing GET/PUT /v8/artifacts/{hash}. |
| Node.js | 18 / 20 / 22 | Runs on all | Node version not auto-hashed — add it manually. |
| pnpm / npm / Yarn | pnpm 8–9, npm 9+, Yarn 3+ | Lockfile is a hash input | Commit the lockfile; uncommitted lockfiles break hashing. |
Related
- esbuild & Turbopack Workflows — the overview covering the incremental and task-level caching layers this guide builds on.
- Configuring Remote Cache with Turborepo and Vercel — the concrete
turbo login/turbo linkand CI-token workflow with verification. - Turbopack Incremental Compilation — the framework-side compilation cache that complements task-level remote caching.
- esbuild & Turbopack Version Compatibility Reference — version pairings that determine which
turbo.jsonschema and cache flags are available.