cheat sheet

turbo

Package-level reference for turbo on npm — install, pipelines, remote cache, filter syntax, and the Nx / pnpm-workspaces comparison.

turbo

What it is

turbo (the npm package) is the CLI for Turborepo — Vercel's incremental task runner and build system for monorepos. It schedules package.json scripts across workspace packages with awareness of dependency topology, hashes every input to skip already-completed work, and ships a hosted remote-cache (Vercel Remote Cache) so CI runners and teammates share build artefacts.

The pitch: tasks declared once in turbo.json, run with turbo run build, and the second invocation of build either reads from local cache (instant) or pulls from remote cache (~100 ms per package). Combined with pnpm workspaces, it's the canonical TypeScript-monorepo stack of the mid-2020s.

Install

bash
# As a dev dep in the monorepo root
npm install -D turbo
pnpm add -wD turbo            # pnpm requires -w for workspace-root
yarn add -DW turbo
bun add -d turbo

# Optional global install for the CLI shorthand
npm install -g turbo

Output: turbo (and shorter aliases) binary on PATH. The binary is platform-specific (downloaded by post-install hook from npm) and weighs ~30 MB — written in Rust under the hood.

bash
# Scaffold a new turborepo
npx create-turbo@latest

Output: generates a monorepo with apps/, packages/, pnpm-workspace.yaml, turbo.json pre-configured.

Versioning & Node support

  • Current major line is 2.x (released 2024) — workspace-config rewrite, turbo.json schema breaking changes, ditched legacy pipeline key in favour of tasks.
  • 1.x (2022-2023) is widely deployed.
  • Recent releases require Node 18+.
  • The Rust core depends on libc; pre-built binaries for Linux glibc, Linux musl (Alpine), macOS x86/arm64, Windows x86/arm64.
  • The hosted remote cache (Vercel Remote Cache) is a separate product; the CLI works without it (local cache only).

Package metadata

  • Maintainer: Vercel.
  • Project home: github.com/vercel/turborepo
  • Docs: turborepo.com
  • npm: npmjs.com/package/turbo
  • License: MPL-2.0 (Mozilla Public License — copyleft for the binary; permissive enough for commercial use).
  • First released: open-sourced 2021 (originally proprietary at Vercel acquisition target).
  • Downloads: ~3-5 million weekly. Adoption is growing fast in the Next.js / pnpm ecosystem.

Peer dependencies & extras

turbo is a self-contained binary with no JS peer deps. The companion ecosystem includes:

Add-onPurpose
create-turboScaffold a fresh turborepo
eslint-config-turboLint rules for turbo.json config and env-var declarations
@turbo/genCode-generation utility (templates for new packages)
tsup / unbuildCommon companion bundlers for library packages
changesetsVersioning + changelogs for monorepo publishes — pairs well

turbo does NOT do package management — pair with pnpm workspaces (the recommended default), npm workspaces, Yarn workspaces, or Bun workspaces.

Alternatives

ToolTrade-off
NxOlder, larger feature set: generators, dependency graph viz, affected-detect, plugin ecosystem. Steeper learning curve. The main competitor; chosen by larger enterprise.
LernaThe OG monorepo manager. Lerna v6+ runs on top of Nx. Mostly legacy.
moonrepoRust-based, newer than Turbo. Smaller community.
BazelGoogle's monorepo tool. Industrial-strength; massive learning curve. Right for huge polyglot orgs.
pnpm workspaces aloneNo task orchestration, just package linking. pnpm -r run build works but no caching, no dep awareness. Combine with turbo for the modern stack.
turbo + nxSome teams use both — Turbo for task running, Nx for code generators and graph viz. Diminishing returns.

Honest take (2026): Nx is more feature-rich (generators, plugins, affected detection). Turbo is simpler, faster to learn, and ships better remote caching out of the box. Pick Turbo for "Next.js + pnpm + a few libraries" and Nx for "10+ apps, multi-framework, generators". For tiny monorepos (2-3 packages), pnpm -r alone may be enough.

Common gotchas

  1. turbo.json tasks key (v2) vs pipeline (v1). Migration is mechanical but breaks every example written pre-2024.
  2. Hash inputs must include EVERY file the task reads. Forgetting to declare tsconfig.json in inputs means a tsconfig change won't bust the cache → silent stale builds. Use globalDependencies for monorepo-wide files.
  3. Env vars NOT in env are stripped from the task environment. This is correct for cache determinism but surprising — process.env.MY_VAR will be undefined until you add "env": ["MY_VAR"] to the task config.
  4. Cross-package output flow uses dependsOn. If app depends on pkg, the task config must declare "dependsOn": ["^build"] to wait for upstream builds. The ^ prefix means "in dependent packages".
  5. Remote cache misses on signed mismatches. Different Node versions, different pnpm-lock.yaml produces different hashes. Pin Node in CI.
  6. turbo prune outputs a different lockfile. For Docker builds, turbo prune --scope=@my/app produces a focused subtree; the lockfile is regenerated and may include fewer / different versions than the root.
  7. Watch mode (turbo watch) is recent and experimental. Production teams still chain concurrently + tsx --watch per-package; native watch is gaining ground but lacks parity with framework dev servers.
  8. The cache key includes turbo.json itself. Editing turbo.json busts every task's cache — minor edits cause a full rebuild.

Real-world recipes

Dev mode — parallel watchers across packages

jsonc
// turbo.json
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
bash
turbo run dev

Output:

text
• Packages in scope: api, web, ui
• Running dev in 3 packages
api:dev: ready on http://localhost:4000
web:dev: ready on http://localhost:3000

persistent: true tells Turbo the task is long-running (doesn't exit); cache: false prevents stale-cache replay of dev-mode output. Each package's dev script (vite, tsx watch, etc.) runs in parallel.

Build with cache

jsonc
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "package.json", "tsconfig.json"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    }
  }
}
bash
turbo run build

Output (cold):

text
• Packages in scope: @my/api, @my/ui, @my/web
• Running build in 3 packages

@my/ui:build:   ✓ built dist/ in 1.2s
@my/api:build:  ✓ built dist/ in 0.8s
@my/web:build:  ✓ built .next/ in 4.1s

 Tasks:    3 successful, 3 total
 Cached:   0 cached, 3 total
 Time:     5.2s

Output (second run, no changes):

text
 Tasks:    3 successful, 3 total
 Cached:   3 cached, 3 total
 Time:     142ms >>> FULL TURBO

Remote cache configuration

The hosted Vercel Remote Cache:

bash
# Log in once
turbo login

# Link the current repo to a team
turbo link

Output:

text
>>> Opening browser to https://vercel.com/api/registration/login-with-github?mode=login
>>> Success! Turborepo CLI authorized for user alice@example.com
? Which Vercel scope do you want to associate "my-repo" with?
✔ Linked to vercel/my-repo

~/.turbo/config.json now holds the access token. Subsequent turbo run build invocations check the remote cache before computing locally. Setup is per-developer; CI uses TURBO_TOKEN env var.

Self-hosted alternative: --api, --token, --team flags or env vars pointing at any S3-compatible cache via a community proxy (turborepo-remote-cache, next-deploy-action, etc.).

yaml
# .github/workflows/ci.yml
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: my-team
steps:
  - run: turbo run build test lint

Filter to specific packages

bash
# Only the web app and its dependencies
turbo run build --filter=@my/web...

# The web app's dependents (downstream)
turbo run test --filter=...@my/ui

# By path
turbo run build --filter='./apps/*'

# Only changed packages since main
turbo run test --filter='[main]'

Output:

text
• Packages in scope: @my/web, @my/ui, @my/utils
• Running build in 3 packages
 Tasks:    3 successful, 3 total
 Time:     2.4s

The filter syntax (... postfix = "and its deps"; ... prefix = "and its dependents"; [ref] = "changed since ref") is dense but powerful — most CI configs lean on [origin/main] to skip unchanged work.

Pipeline definition (cross-package ordering)

jsonc
// turbo.json
{
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test":  { "dependsOn": ["build"] },
    "lint":  {},
    "deploy": { "dependsOn": ["build", "test"], "cache": false }
  }
}
  • ^build means "build of every dependency package first".
  • build (no caret) within test's dependsOn means "build of THIS package first".
  • lint has no deps — runs in parallel with everything else.

Code generation with @turbo/gen

bash
npx turbo gen

Output:

text
? What generator would you like to run? (Use arrow keys)
❯ react-package
  cli-tool
  config-package
? Package name: new-pkg
✔ packages/new-pkg created

Walks you through scaffolding a new package using templates in turbo/generators/. Decent built-in support for "spin up a new internal package quickly".

Pruning for Docker builds

bash
turbo prune --scope=@my/web --docker

Output:

text
Generating pruned monorepo for @my/web in ./out
 - Added @my/web
 - Added @my/ui
 - Added @my/utils

Outputs an out/ directory with:

  • json/ — a minimal package.json per workspace package needed by @my/web
  • full/ — the actual source files
  • A regenerated pnpm-lock.yaml / package-lock.json

The standard Docker pattern:

dockerfile
FROM node:20-alpine AS pruner
WORKDIR /app
COPY . .
RUN npx turbo prune --scope=@my/web --docker

FROM node:20-alpine AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo run build --filter=@my/web

This produces a Docker image with ONLY the deps and source needed by @my/web.

Production deployment

turbo itself is build-time, not runtime — but the artefacts it produces ship.

CI configuration

yaml
# .github/workflows/ci.yml
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: my-team
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }   # Required for [main] filter
      - uses: pnpm/action-setup@v3
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "pnpm" }
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run build test lint --filter=[origin/main]

fetch-depth: 0 is required for [origin/main] git-comparison filtering. The --filter=[origin/main] cuts CI time dramatically — only build packages touched by the PR.

Cache backends

BackendSetupWhen
Vercel Remote Cacheturbo login && turbo linkDefault; managed; free up to a threshold
S3-compatible (Cloudflare R2, MinIO)ducktors/turborepo-remote-cache proxySelf-hosted preference
Filesystem (NFS / shared volume)Set TURBO_CACHE_DIR to shared pathAir-gapped environments

Performance tuning

Concurrency

bash
turbo run build --concurrency=4    # Bound parallel tasks
turbo run build --concurrency=50%  # Half of CPU cores

Output:

text
• Packages in scope: api, web, ui
 Tasks:    3 successful, 3 total
 Time:     6.8s

Default is 10 — fine for most monorepos but constrains huge ones.

Profile output

bash
turbo run build --profile=profile.json

Output:

text
 Tasks:    3 successful, 3 total
 Time:     5.4s
Profile written to profile.json

Opens in chrome://tracing — see exactly which packages are on the critical path.

Reduce hash inputs

Over-broad inputs (["**/*"]) bust the cache on every file change. Narrow to what each task actually reads:

jsonc
{
  "tasks": {
    "build": {
      "inputs": ["src/**", "package.json", "tsconfig.json"]
    }
  }
}

Turn off output capture for verbose tasks

outputLogs: "errors-only" saves CI log space; outputLogs: "new-only" skips cached-task logs.

Version migration guide

1 → 2 (major)

turbo.json schema rewrite:

jsonc
// v1 (deprecated)
{
  "pipeline": {
    "build": { "dependsOn": ["^build"] }
  }
}

// v2
{
  "tasks": {
    "build": { "dependsOn": ["^build"] }
  }
}

The turbo migrate codemod handles most cases:

bash
npx @turbo/codemod migrate

Output:

text
? Which Turborepo transform would you like to apply? › migrate
✔ Detected turbo.json
✔ Migrated turbo.json (pipeline → tasks)
✔ Done in 0.3s

Other v2 changes:

  • globalEnv and env are now strictly enforced — task fails if it reads an undeclared var (was a warning in v1).
  • outputs no longer defaults to dist/** — must be declared per-task.
  • dotEnv removed from globalDependencies — use globalDependencies: [".env"] or per-task inputs.

Security considerations

  1. TURBO_TOKEN is a write credential. Anyone with it can upload cache entries to your team's remote cache. Treat like an API key.
  2. Cache poisoning is a real concern. If you allow PR builds to write to the cache, a malicious contributor could upload a tampered build artefact that main-branch builds then read. Disable remote-cache writes for forks: TURBO_CACHE_DIR=local for PR jobs.
  3. env declarations control what's hashed. If a secret like DATABASE_URL is in env, it's part of the cache key; rotating the secret invalidates every cached build. Use passThroughEnv for vars the task needs but shouldn't affect the hash.
  4. The cache directory may contain source code. Local .turbo/ and remote cache hold compressed task outputs — protect them like build artefacts.

Ecosystem integrations

ToolHow it fits
pnpm workspacesThe canonical package manager. pnpm-workspace.yaml declares packages; turbo consumes the layout.
Next.jsFirst-class support; create-turbo scaffolds a Next + shared-UI monorepo by default.
changesetsVersioning + changelogs. pnpm changeset + pnpm changeset version + turbo run publish-packages.
tsup / unbuildPer-package bundlers — popular for library packages in a turborepo.
Dockerturbo prune --docker produces a focused subtree for image builds.
GitHub ActionsFirst-class CI integration; setup-node cache + TURBO_TOKEN is the standard combo.

When NOT to use this

  • Single-package projects. Turbo's value is task orchestration across packages. For one package, just use npm/pnpm scripts directly.
  • Tiny monorepos (2-3 packages, no cross-deps). pnpm -r run build is enough; the Turbo overhead isn't worth it.
  • Polyglot monorepos with substantial Java / Go / Python. Bazel handles cross-language orchestration; Turbo is JS-centric (though it can run any shell command).
  • You need generators and a strong plugin ecosystem. Nx has more out-of-the-box; consider it instead.
  • Air-gapped CI without an external cache. Turbo works without remote cache — but you lose the biggest benefit. Self-host with the community proxy or accept local-only caching.
  • Your team prefers Lerna / Rush / yarn-workspaces only. Migration is real work; weigh the speed gain.

The sweet spot: a Next.js or React monorepo with 3-20 packages, pnpm workspaces, and CI that runs build + test + lint. Turbo cuts CI times by 50-90% in that shape.

See also