Configuring Remote Cache with Turborepo and Vercel

This guide walks through enabling Vercel Remote Cache for an existing Turborepo so that builds computed locally or on one CI runner are restored as cache hits everywhere else. For the hashing model and the artifact protocol underneath this setup, read Remote Caching and Distributed Build Coordination first — here we focus narrowly on the turbo login/turbo link handshake, supplying TURBO_TOKEN/TURBO_TEAM in headless CI, and proving with --summarize and --remote-only that hits are genuinely coming from the remote store rather than a warm local cache.

Local and CI machines sharing a Vercel Remote Cache A developer links the repo with turbo login and turbo link, while CI authenticates with TURBO_TOKEN and TURBO_TEAM, both reading and writing the same Vercel Remote Cache. Developer laptop turbo login turbo link CI runner TURBO_TOKEN TURBO_TEAM Vercel Remote Cache GET/PUT /v8/artifacts scoped by teamId read + write artifacts read + write artifacts Same hash on either machine resolves to the same artifact.
Figure: a developer machine and a CI runner authenticating to the same Vercel Remote Cache, one via interactive login, one via token.

Prerequisites & Reproducible Setup

You need Turborepo 2.x in a workspace-enabled monorepo and a Vercel account (the cache is free on the hobby tier for the cache feature itself). Install and confirm the version:

# Turborepo 2.x. Run from the monorepo root.
pnpm add -Dw turbo@^2          # or: npm i -D turbo@^2 / yarn add -D turbo@^2
npx turbo --version            # expect 2.x.y

# A minimal turbo.json must exist with cacheable tasks and outputs.
cat > turbo.json <<'JSON'
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    }
  }
}
JSON

Before involving the remote, confirm local caching is healthy — npx turbo run build twice should end with >>> FULL TURBO. Remote caching only shares what local caching already produces correctly.

Diagnosis Workflow: Linking and Verifying

  1. Authenticate the machine. npx turbo login opens a browser, authenticates against Vercel, and stores a token at ~/.turbo/config.json. In a container without a browser, skip this and use the token method below.
  2. Link the repository to a team’s cache. npx turbo link prompts for the Vercel scope (team) and writes the team id into .turbo/config.json in the repo. From now on every turbo run reads and writes that team’s remote cache.
  3. Force a remote round-trip. Run npx turbo run build --remote-only to disable the local cache and prove the artifact moves through Vercel. The first run uploads (cache miss, executing); a second --remote-only run on a freshly-cleared local cache should restore from remote.
  4. Inspect what turbo actually did. Add -vv to surface HTTP traffic, or --summarize to write a JSON run report and read each task’s cache.source.

Complete Solution: Config, Token, and CI

For headless CI there is no interactive turbo login. Mint a Vercel access token (Account Settings → Tokens), store it and the team slug as CI secrets, and pass them as environment variables turbo reads automatically.

# .github/workflows/ci.yml — Turborepo 2.x, Node 20, pnpm.
name: CI
on:
  pull_request:
  push:
    branches: [main]

env:
  # turbo reads these to authenticate against Vercel Remote Cache headlessly.
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}   # a Vercel access token
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}        # your team slug, e.g. "team_acme"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # turbo needs git history to scope affected tasks
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      # --remote-only forces hits/misses through Vercel (no local cache in a
      # clean runner anyway), and --summarize writes a report we assert on.
      - run: pnpm exec turbo run build test lint --remote-only --summarize
      # Fail the job if NOTHING was a remote hit on a no-op change (optional).
      - name: Show cache sources
        run: |
          jq '.tasks[] | {task: .taskId, status: .cache.status, source: .cache.source}' \
            .turbo/runs/*.json

To require that downloaded artifacts were produced by a trusted machine, enable signature verification. Vercel Remote Cache supports the x-artifact-tag HMAC header; turn it on in turbo.json and provide the shared key:

// turbo.json — add artifact signing. The key lives in env, never in the file.
{
  "$schema": "https://turborepo.com/schema.json",
  "remoteCache": { "signature": true },
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }
  }
}
# Provide the HMAC key in every environment that reads or writes the cache.
# A downloaded artifact whose signature fails verification is rejected and
# the task is re-run locally instead of trusting tampered bytes.
export TURBO_REMOTE_CACHE_SIGNATURE_KEY="<32+ byte shared secret>"

Verification

Confirm remote hits with two commands. First, clear the local cache and run with --remote-only and --summarize:

# Turborepo 2.x. Prove a hit came from Vercel, not a warm local cache.
rm -rf node_modules/.cache/turbo
TURBO_TOKEN="$TURBO_TOKEN" TURBO_TEAM="$TURBO_TEAM" \
  npx turbo run build --remote-only --summarize

# Read the cache source for every task from the generated run report.
jq '.tasks[] | {task: .taskId, status: .cache.status, source: .cache.source}' \
  .turbo/runs/*.json

A successful remote restore shows "status": "HIT" with "source": "REMOTE" for each task, and the run finishes in seconds. The inline log also prints cache hit, replaying logs per task and a final >>> FULL TURBO. If you see "source": "LOCAL", your local cache was not actually cleared; if you see MISS, either the artifact was never uploaded or a hash input differs from the machine that seeded it.

Gotchas & Edge Cases

  • TURBO_TEAM must be the slug, not the display name. Use the value from the team’s URL (team_acme), or the team id from .turbo/config.json after a successful turbo link. A wrong slug yields a silent fall-back to local-only with a 403 visible under -vv.
  • Don’t commit .turbo/config.json secrets. turbo link writes a team id (safe), but tokens belong in CI secrets and ~/.turbo/config.json, never in the repo. Add .turbo log artifacts to .gitignore.
  • fetch-depth: 0 matters. Turborepo uses git to scope --filter and affected-package detection; a shallow checkout can over- or under-run tasks and muddy your hit-rate measurements.
  • Pass-through vs hashed env vars. TURBO_TOKEN/TURBO_TEAM/CI should sit in globalPassThroughEnv so they authenticate without polluting the cache key; only output-affecting variables belong in a task’s env. Mixing these up is the most common cause of “works locally, misses in CI.”