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.

Local task graph resolving to a remote cache hit or miss across CI machines A package task graph is hashed, then queried against a shared remote cache; one CI machine records a hit and replays output while another records a miss and uploads a fresh artifact. Local task graph ui#build hash 9f3a.. web#build deps: ui#build web#test hash c71e.. Remote cache GET /artifacts/{hash} PUT /artifacts/{hash} CI machine A HIT — replay output 0.4s, no rebuild CI machine B MISS — run + upload new hash, PUT artifact Hash inputs (any change flips the key) source files in the package + declared inputs (globs) resolved dependency versions from the lockfile listed env vars + global deps + the task's own command hashes of upstream tasks (dependsOn ^build) turbo + turbo.json schema version untracked env or OS-specific output → spurious miss
Figure: a hashed package task graph queried against a shared remote cache, producing a hit on one CI machine and a miss-plus-upload on another.

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-level pipeline key to tasks in turbo.json, tightened env-var handling, and stabilized --remote-only/--summarize output. 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-tag header 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

  1. Write turbo.json with explicit outputs and env. A task with no outputs caches 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'.
  2. Prove local caching works first. Run npx turbo run build twice. 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.
  3. Connect the remote cache. Run npx turbo login then npx turbo link for Vercel, or export TURBO_API/TURBO_TOKEN/TURBO_TEAM for a self-hosted server.
  4. Seed the cache from a clean machine. Run npx turbo run build --remote-only so artifacts are forced through the remote backend (local cache disabled), populating the store.
  5. Wire CI to share the cache. Set TURBO_TOKEN and TURBO_TEAM as CI secrets and run npx turbo run build test lint. The concrete GitHub Actions wiring is detailed in Configuring Remote Cache with Turborepo and Vercel.
  6. Verify cross-machine hits. Clear the local cache (rm -rf node_modules/.cache/turbo), then run npx turbo run build --summarize. Open the generated .turbo/runs/*.json summary 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 pipelinetasks 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.

In-Depth Guides