Turborepo 2.0: Remote Caching, Task Pipelines, and What Actually Speeds Up CI
Turborepo 2.0 shipped with a rewritten task runner, first-class Watch mode, and a new turbo.json schema. After migrating two monorepos to 2.0, here's what the upgrade actually looks like.
Why Turborepo Exists
A monorepo without a build orchestrator means every npm run build rebuilds everything. 5 packages? Fine. 20 packages? You're waiting 8 minutes for CI on a 2-line change.
Turborepo solves this with:
- Task graph: understands which packages depend on which
- Local cache: skips tasks whose inputs haven't changed
- Remote cache: shares that cache across every machine and CI runner
The 2.0 Schema Changes
The pipeline key is gone. Tasks are now defined directly:
// turbo.json (v2)
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": []
}
}
}
Key change: "pipeline" → "tasks". Automated migration:
npx @turbo/codemod migrate
Runs in ~30 seconds, handles the rename and schema normalization.
Understanding dependsOn
This is where most people get confused.
^build — run build in all dependency packages first, then this package.
build (no caret) — run build in this package first, then this task.
[] — no dependencies, run immediately and in parallel.
{
"tasks": {
"build": { "dependsOn": ["^build"] },
"test": { "dependsOn": ["build"] },
"lint": { "dependsOn": [] }
}
}
With this config: all packages lint in parallel, build respects dependency order, tests wait for local build.
Remote Caching Setup
Local cache is on by default. Remote cache requires a Vercel account (free) or a self-hosted Turborepo Remote Cache server.
Vercel Remote Cache
npx turbo login
npx turbo link
That's it. Now every turbo build on any machine shares artifacts. CI goes from 6 minutes to 45 seconds on a warm cache.
Self-Hosted Cache (if you can't use Vercel)
# ducktape/turborepo-remote-cache is the reference implementation
docker run -p 3000:3000 \
-e TURBO_TOKEN=your-secret \
ducktape/turborepo-remote-cache
Then in your CI:
TURBO_API="https://your-cache.internal" \
TURBO_TOKEN="your-secret" \
TURBO_TEAM="myteam" \
npx turbo build
Watch Mode (New in 2.0)
The biggest developer experience win in 2.0:
turbo watch dev
Spins up dev scripts across all packages and automatically restarts affected packages when files change. Previously you needed concurrently or custom scripts to orchestrate this.
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}
persistent: true tells Turbo this task doesn't exit — don't wait for it before running dependents.
Filtering Runs
Only run tasks for changed packages:
# Only run affected packages since main
turbo build --filter='...[origin/main]'
# Only a specific package and its dependents
turbo build --filter='@acme/ui...'
# Exclude a package
turbo build --filter='!@acme/storybook'
In GitHub Actions:
- name: Build affected packages
run: turbo build --filter='...[HEAD^1]'
On a monorepo with 15 packages, this typically cuts CI time by 60-80% for single-package PRs.
What Actually Moves the Needle
After optimizing three monorepos, the impact ranking:
- Remote cache — 10x improvement on repeated CI runs for unchanged code
-
--filteron CI — 3-5x improvement for typical feature PRs -
Correct
dependsOn— 1.5-2x improvement by maximizing parallelism -
outputsconfiguration — ensures cache hits are valid, prevents stale artifact bugs
The single biggest mistake: not configuring outputs. Without it, Turbo can't restore cached builds correctly.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"]
}
}
}
Match your actual build output directories. Turbo stores and restores these on cache hit.
Common Gotchas
Environment variables aren't automatically hashed. Add them to globalEnv or per-task env or Turbo won't bust the cache when they change:
{
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL"]
}
}
}
Don't cache dev tasks. They're stateful and long-running. "cache": false is the correct setting.
Workspaces must be properly linked. Turbo reads your package manager's workspace config (workspaces in root package.json for npm/yarn, packages in pnpm-workspace.yaml). Mis-configured workspaces mean Turbo can't resolve the dependency graph.
The Verdict
Turborepo 2.0 is a meaningful upgrade. The new task runner is faster, Watch mode eliminates a whole category of custom scripts, and the schema cleanup reduces cognitive overhead.
If you're on 1.x, the migration is a 30-second codemod and worth doing today. If you're evaluating Nx vs Turborepo — Turbo wins on simplicity for TS/JS monorepos without custom generators or module federation.
Building a multi-package TypeScript project and want the foundation already wired? The Ship Fast Skill Pack includes Turborepo setup, shared ESLint/TypeScript configs, and 20+ Claude Code workflow skills for $49.
Top comments (0)