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
# 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.
# 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.jsonschema breaking changes, ditched legacypipelinekey in favour oftasks. 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-on | Purpose |
|---|---|
create-turbo | Scaffold a fresh turborepo |
eslint-config-turbo | Lint rules for turbo.json config and env-var declarations |
@turbo/gen | Code-generation utility (templates for new packages) |
tsup / unbuild | Common companion bundlers for library packages |
changesets | Versioning + 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
| Tool | Trade-off |
|---|---|
| Nx | Older, larger feature set: generators, dependency graph viz, affected-detect, plugin ecosystem. Steeper learning curve. The main competitor; chosen by larger enterprise. |
| Lerna | The OG monorepo manager. Lerna v6+ runs on top of Nx. Mostly legacy. |
| moonrepo | Rust-based, newer than Turbo. Smaller community. |
| Bazel | Google's monorepo tool. Industrial-strength; massive learning curve. Right for huge polyglot orgs. |
| pnpm workspaces alone | No task orchestration, just package linking. pnpm -r run build works but no caching, no dep awareness. Combine with turbo for the modern stack. |
turbo + nx | Some 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
turbo.jsontaskskey (v2) vspipeline(v1). Migration is mechanical but breaks every example written pre-2024.- Hash inputs must include EVERY file the task reads. Forgetting to declare
tsconfig.jsonininputsmeans a tsconfig change won't bust the cache → silent stale builds. UseglobalDependenciesfor monorepo-wide files. - Env vars NOT in
envare stripped from the task environment. This is correct for cache determinism but surprising —process.env.MY_VARwill be undefined until you add"env": ["MY_VAR"]to the task config. - Cross-package output flow uses
dependsOn. Ifappdepends onpkg, the task config must declare"dependsOn": ["^build"]to wait for upstream builds. The^prefix means "in dependent packages". - Remote cache misses on signed mismatches. Different Node versions, different
pnpm-lock.yamlproduces different hashes. Pin Node in CI. turbo pruneoutputs a different lockfile. For Docker builds,turbo prune --scope=@my/appproduces a focused subtree; the lockfile is regenerated and may include fewer / different versions than the root.- Watch mode (
turbo watch) is recent and experimental. Production teams still chainconcurrently+tsx --watchper-package; native watch is gaining ground but lacks parity with framework dev servers. - The cache key includes
turbo.jsonitself. Editingturbo.jsonbusts every task's cache — minor edits cause a full rebuild.
Real-world recipes
Dev mode — parallel watchers across packages
// turbo.json
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}
turbo run dev
Output:
• 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
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
}
}
}
turbo run build
Output (cold):
• 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):
Tasks: 3 successful, 3 total
Cached: 3 cached, 3 total
Time: 142ms >>> FULL TURBO
Remote cache configuration
The hosted Vercel Remote Cache:
# Log in once
turbo login
# Link the current repo to a team
turbo link
Output:
>>> 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.).
# .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
# 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:
• 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)
// turbo.json
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] },
"lint": {},
"deploy": { "dependsOn": ["build", "test"], "cache": false }
}
}
^buildmeans "build of every dependency package first".build(no caret) withintest'sdependsOnmeans "build of THIS package first".linthas no deps — runs in parallel with everything else.
Code generation with @turbo/gen
npx turbo gen
Output:
? 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
turbo prune --scope=@my/web --docker
Output:
Generating pruned monorepo for @my/web in ./out
- Added @my/web
- Added @my/ui
- Added @my/utils
Outputs an out/ directory with:
json/— a minimalpackage.jsonper workspace package needed by@my/webfull/— the actual source files- A regenerated
pnpm-lock.yaml/package-lock.json
The standard Docker pattern:
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
# .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
| Backend | Setup | When |
|---|---|---|
| Vercel Remote Cache | turbo login && turbo link | Default; managed; free up to a threshold |
| S3-compatible (Cloudflare R2, MinIO) | ducktors/turborepo-remote-cache proxy | Self-hosted preference |
| Filesystem (NFS / shared volume) | Set TURBO_CACHE_DIR to shared path | Air-gapped environments |
Performance tuning
Concurrency
turbo run build --concurrency=4 # Bound parallel tasks
turbo run build --concurrency=50% # Half of CPU cores
Output:
• 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
turbo run build --profile=profile.json
Output:
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:
{
"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:
// v1 (deprecated)
{
"pipeline": {
"build": { "dependsOn": ["^build"] }
}
}
// v2
{
"tasks": {
"build": { "dependsOn": ["^build"] }
}
}
The turbo migrate codemod handles most cases:
npx @turbo/codemod migrate
Output:
? Which Turborepo transform would you like to apply? › migrate
✔ Detected turbo.json
✔ Migrated turbo.json (pipeline → tasks)
✔ Done in 0.3s
Other v2 changes:
globalEnvandenvare now strictly enforced — task fails if it reads an undeclared var (was a warning in v1).outputsno longer defaults todist/**— must be declared per-task.dotEnvremoved fromglobalDependencies— useglobalDependencies: [".env"]or per-taskinputs.
Security considerations
TURBO_TOKENis a write credential. Anyone with it can upload cache entries to your team's remote cache. Treat like an API key.- 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=localfor PR jobs. envdeclarations control what's hashed. If a secret likeDATABASE_URLis inenv, it's part of the cache key; rotating the secret invalidates every cached build. UsepassThroughEnvfor vars the task needs but shouldn't affect the hash.- The cache directory may contain source code. Local
.turbo/and remote cache hold compressed task outputs — protect them like build artefacts.
Ecosystem integrations
| Tool | How it fits |
|---|---|
| pnpm workspaces | The canonical package manager. pnpm-workspace.yaml declares packages; turbo consumes the layout. |
| Next.js | First-class support; create-turbo scaffolds a Next + shared-UI monorepo by default. |
| changesets | Versioning + changelogs. pnpm changeset + pnpm changeset version + turbo run publish-packages. |
| tsup / unbuild | Per-package bundlers — popular for library packages in a turborepo. |
| Docker | turbo prune --docker produces a focused subtree for image builds. |
| GitHub Actions | First-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 buildis 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
- JavaScript: turbo — CLI flags, config patterns, monorepo workflow
- Concept: pipes — task orchestration mental model
- Packages: npm-concurrently — simpler in-package script orchestration
- Packages: npm-npm-run-all2 — single-package script chains