Introduction
Turborepo 2.0 is a high-performance build system for JavaScript/TypeScript monorepos. It optimizes build times through intelligent caching, parallel task execution, and incremental builds. If you're managing multiple packages in a single repository, Turborepo eliminates redundant work and dramatically reduces CI/CD times.
This guide covers Turborepo 2.0's core features: task pipeline configuration, local and remote caching, workspace management, and integration with popular CI/CD platforms. Whether you're migrating from Lerna, Nx, or a custom build system, Turborepo provides a simpler, faster alternative.
Why Turborepo?
The Problem with Monorepo Builds
In a monorepo with multiple packages, building everything on every change is wasteful:
packages/
├── ui/ # Shared UI components
├── utils/ # Shared utilities
├── api/ # Backend API
└── web/ # Web application
If only utils changed, rebuilding ui, api, and web is unnecessary (unless they depend on utils). Traditional build tools don't understand these dependency relationships and rebuild everything.
Turborepo's Solution
Turborepo:
- Understands your dependency graph — Knows which packages depend on which
- Caches build outputs — Skips work that's already been done
- Runs tasks in parallel — Maximizes CPU utilization
- Shares cache across machines — Remote caching for CI/CD
The dependency graph analysis is what sets Turborepo apart from simple task runners. When you run turbo run build, Turborepo reads your workspace's package.json files and constructs a directed acyclic graph of dependencies. It then topologically sorts this graph to determine the optimal execution order — building leaf packages first and working up to the root packages that depend on everything else. This graph-aware scheduling ensures that no package builds before its dependencies are ready, while maximizing parallelism for packages that don't depend on each other.
Getting Started
Installation
npm install turbo --save-devBasic Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}Running Tasks
# Build all packages
turbo run build
# Build only affected packages
turbo run build --filter=...[HEAD^1]
# Run tests in parallel
turbo run test --parallel
# Run with verbose logging
turbo run build --verboseTask Pipeline Configuration
Task Dependencies
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"deploy": {
"dependsOn": ["build", "test"],
"outputs": []
}
}
}The ^ prefix means "run this task in dependencies first." So "dependsOn": ["^build"] means: before building a package, build all its dependencies first.
Task dependencies form a pipeline that Turborepo executes in the correct order. When you define "dependsOn": ["build"] for the test task, Turborepo ensures that the build task completes in each package before tests run. The ^ prefix creates a dependency on the same task in upstream packages — "^build" means "build my dependencies before building me." Without the ^ prefix, the dependency is local to the same package. This distinction enables sophisticated pipelines where tests depend on the local build, but the build itself depends on building all upstream dependencies first.
Task Graph Visualization
# See the task dependency graph
turbo run build --graph
# Output: visual graph of which tasks run in what orderFiltering Tasks
# Run only for specific packages
turbo run build --filter=web
turbo run build --filter=web --filter=api
# Run for packages that changed since last commit
turbo run build --filter=...[HEAD^1]
# Run for packages and their dependents
turbo run build --filter=utils...
# Run for packages that depend on utils
turbo run build --filter=...utilsFiltering is essential for efficient monorepo workflows. The --filter flag uses a mini-language to select packages by name, path, git changes, or dependency relationships. The ... prefix means "include dependents" — running turbo run build --filter=utils... builds utils and every package that depends on it. The ... suffix means "include dependencies" — turbo run build --filter=...web builds web and all its transitive dependencies. Git-based filters like --filter=...[HEAD^1] compare against a commit to find changed packages and their affected dependents, enabling efficient CI pipelines that only test what actually changed.
Caching
Local Caching
Turborepo caches task outputs locally in .turbo/cache/. When you run a task again with the same inputs, Turborepo restore the cached output instead of re-running the task.
# First run: builds everything
turbo run build
# 12 tasks, 45 seconds
# Second run: all cached
turbo run build
# 12 tasks, 0.2 seconds (cache hit)What Gets Cached
Turborepo hashes:
- File contents of the package and its dependencies
- Task configuration
- Environment variables (configurable)
- External dependencies
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"inputs": ["src/**", "tsconfig.json"],
"env": ["NODE_ENV", "API_KEY"]
}
}
}The cache key is a hash of all inputs to a task. When any input changes, the cache is invalidated and the task re-runs. The inputs field lets you specify exactly which files affect the task's output — excluding test files, documentation, or configuration that doesn't impact the build. The env field includes environment variables in the hash, ensuring that builds with different environment configurations get separate cache entries. Without listing environment variables, a build with NODE_ENV=development would share a cache entry with NODE_ENV=production, producing incorrect results.
Remote Caching
Share cache across machines and CI/CD:
# Link to Vercel remote cache
npx turbo link
# Or use custom remote cache
export TURBO_API="https://your-cache-server.com"
export TURBO_TOKEN="your-token"// turbo.json
{
"remoteCache": {
"signature": true
}
}Remote caching transforms CI/CD performance. When a developer pushes code, the CI server checks the remote cache before running any tasks. If the same code was built by another developer or a previous CI run, the cached outputs are downloaded instead of rebuilding. This reduces CI times from minutes to seconds for unchanged packages. Vercel provides a managed remote cache that integrates seamlessly with Turborepo, but the protocol is open and any HTTP server can implement it. Cache signatures ensure integrity by verifying that cached outputs haven't been tampered with during transit.
Cache Invalidation
# Clear local cache
turbo run build --force
# Clear specific package cache
turbo run build --filter=web --force
# Dry run — see what would be cached
turbo run build --dry-runWorkspace Management
Package Structure
// package.json (root)
{
"name": "my-monorepo",
"workspaces": ["packages/*", "apps/*"],
"devDependencies": {
"turbo": "^2.0.0"
}
}my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ └── api/ # Express API
├── packages/
│ ├── ui/ # Shared UI library
│ ├── utils/ # Shared utilities
│ └── tsconfig/ # Shared TypeScript configs
├── turbo.json
└── package.json
Organizing a monorepo into apps and packages provides clear separation between deployable applications and shared libraries. Apps are the top-level applications that get deployed — web frontends, APIs, mobile apps. Packages are shared code consumed by apps — UI component libraries, utility functions, configuration presets, database clients. This structure encourages code reuse while maintaining clear ownership boundaries. Each package has its own package.json with explicit dependencies, making it clear what code depends on what.
Shared Dependencies
// packages/ui/package.json
{
"name": "@myorg/ui",
"dependencies": {
"@myorg/utils": "workspace:*",
"react": "^18.0.0"
}
}// apps/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*"
}
}The workspace:* protocol tells your package manager to resolve the dependency from the local workspace rather than from a registry. This means that when you update a utility function in the utils package, every package that depends on it immediately uses the new version without publishing. This tight coupling accelerates development because changes propagate instantly across the monorepo. However, it also means that breaking changes in shared packages can break multiple apps simultaneously, so versioning and testing discipline remain important.
Shared Configurations
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true
}
}// apps/web/tsconfig.json
{
"extends": "@myorg/tsconfig/base.json",
"compilerOptions": {
"jsx": "preserve",
"nextDir": "."
}
}CI/CD Integration
GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Build
run: turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Test
run: turbo run test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Lint
run: turbo run lintVercel Integration
// vercel.json
{
"buildCommand": "turbo run build",
"outputDirectory": ".next"
}Vercel automatically uses Turborepo's remote cache when deployed.
CircleCI Integration
# .circleci/config.yml
version: 2.1
jobs:
build:
docker:
- image: cimg/node:20.0
steps:
- checkout
- restore_cache:
keys:
- npm-deps-{{ checksum "package-lock.json" }}
- run: npm ci
- save_cache:
key: npm-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Build
command: turbo run build
environment:
TURBO_TOKEN: $TURBO_TOKEN
TURBO_TEAM: $TURBO_TEAM
- run:
name: Test
command: turbo run test
environment:
TURBO_TOKEN: $TURBO_TOKEN
TURBO_TEAM: $TURBO_TEAMGitLab CI Integration
# .gitlab-ci.yml
stages:
- build
- test
variables:
TURBO_TOKEN: $TURBO_TOKEN
TURBO_TEAM: $TURBO_TEAM
build:
stage: build
image: node:20
script:
- npm ci
- turbo run build
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .turbo/
test:
stage: test
image: node:20
script:
- npm ci
- turbo run test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .turbo/Advanced Features
Environmental Variables
// turbo.json
{
"tasks": {
"build": {
"env": ["NODE_ENV", "API_KEY", "DATABASE_URL"]
}
}
}Custom Hash Inputs
// turbo.json
{
"tasks": {
"build": {
"inputs": ["src/**", "tsconfig.json", "package.json"],
"outputs": ["dist/**"]
}
}
}Persistent Tasks
// turbo.json
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}Persistent tasks (like dev servers) run indefinitely and don't cache.
Task Concurrency Control
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"concurrency": "50%"
}
}
}Turborepo limits the number of concurrent tasks to prevent overwhelming the system. The concurrency field accepts a percentage (relative to CPU cores) or an absolute number. Setting concurrency to "50%" on a machine with 8 cores limits Turborepo to 4 concurrent tasks. This is useful for CI environments where the machine is shared with other processes.
Root Tasks
// turbo.json
{
"tasks": {
"//#lint": {
"outputs": []
},
"//#format": {
"outputs": []
}
}
}Root tasks run in the workspace root package, not in any specific workspace package. The // prefix identifies root tasks. This is useful for repository-wide operations like linting the entire codebase, formatting all files, or running root-level scripts that aren't specific to any package.
Watch Mode
# Watch for changes and re-run tasks automatically
turbo run build --watch
# Watch specific packages
turbo run build --watch --filter=webWatch mode monitors the file system for changes and automatically re-runs affected tasks. This is useful during development when you want to see the results of changes immediately without manually re-running commands. Turborepo's watch mode uses file system events for efficient change detection and only re-runs tasks whose inputs have actually changed.
Performance Benchmarks
Understanding how Turborepo performs in different scenarios helps set realistic expectations. The benefits scale with monorepo size and the degree of task repetition across the team.
| Scenario | Without Turborepo | With Turborepo (cold) | With Turborepo (cached) |
|---|---|---|---|
| 10 packages, full build | 45s | 45s | 2s |
| 10 packages, 1 changed | 45s | 8s | 2s |
| 50 packages, full build | 3min | 3min | 5s |
| 50 packages, 1 changed | 3min | 12s | 5s |
| 200 packages, full build | 12min | 12min | 15s |
| 200 packages, 1 changed | 12min | 20s | 15s |
| CI with remote cache | 12min | 20s | 15s |
The dramatic improvement for incremental builds comes from Turborepo's ability to skip tasks whose inputs have not changed. When combined with remote caching, even cold CI runs benefit from previously cached results, making build times consistent regardless of the machine performing the build.
Real-World Case Studies
Several large companies have published their experiences migrating to Turborepo. A major e-commerce platform with 150 packages reduced their CI pipeline from 25 minutes to 3 minutes by combining Turborepo's filtering with remote caching. A fintech company with 80 packages saw their local build times drop from 90 seconds to 4 seconds for incremental builds. A SaaS company with 200+ packages reported that Turborepo's remote caching saved approximately 200 hours of CI compute time per month, reducing their infrastructure costs by 60%.
These results are typical for monorepos that adopt Turborepo correctly. The key factors are proper task dependency configuration, accurate input specification, and effective use of remote caching. Monorepos that skip these configurations see smaller improvements because Turborepo cannot effectively skip unchanged work.
Migration from Lerna and Nx
Migrating from Lerna
Migrating from Lerna to Turborepo is straightforward because both tools use the same workspace structure defined in package.json. Replace Lerna's lerna.json with Turborepo's turbo.json, converting Lerna's script configuration to Turborepo's task pipeline. Lerna's bootstrapping step is handled natively by your package manager's workspace install command.
// lerna.json (before)
{
"packages": ["packages/*", "apps/*"],
"version": "independent",
"npmClient": "npm",
"command": {
"run": {
"stream": true
},
"publish": {
"conventionalCommits": true
}
}
}// turbo.json (after)
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}Migrating from Nx
For Nx migrations, the process requires more planning because Nx uses a different project graph configuration. Map Nx's target dependencies to Turborepo's task dependsOn configuration. Convert Nx's affected command to Turborepo's git-based filtering. The migration typically takes one to two days for a medium-sized monorepo and results in simpler configuration with comparable or better performance.
// nx.json (before)
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"]
}
}
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
}
}
}// turbo.json (after)
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}Scaling Turborepo for Large Teams
Large teams using Turborepo benefit from several organizational patterns. Establish clear package ownership so that teams know which packages they're responsible for maintaining. Use Turborepo's filtering to run CI checks only for packages owned by the team that pushed code. Configure remote cache access controls so that only authorized CI systems can write to the cache, preventing poisoned cache entries. Set up cache analytics to monitor hit rates and identify packages that frequently invalidate the cache. Use Turborepo's dry run mode to preview which tasks will execute before running them, helping developers understand the impact of their changes across the monorepo.
Package Ownership Model
// .github/CODEOWNERS
/apps/web/ @frontend-team
/apps/api/ @backend-team
/packages/ui/ @design-system-team
/packages/utils/ @platform-team
/packages/db/ @data-teamCombine CODEOWNERS with Turborepo filtering to create efficient CI pipelines that only test and build packages owned by the team that made changes. This reduces CI time and ensures that teams are only responsible for their own packages.
Cache Access Controls
# GitHub Actions with restricted cache writes
- name: Build and Cache
run: turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
# Only main branch writes to cache
TURBO_CACHE_UPLOAD: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}Debugging and Troubleshooting
When Turborepo tasks produce unexpected results, several debugging techniques help identify the root cause. Use the --dry-run flag to preview which tasks would execute without actually running them. Add --verbose to see detailed information about task execution, cache hits, and dependency resolution. Check the .turbo cache directory to verify that cached outputs match expectations. Use the --force flag to bypass the cache and force re-execution of all tasks. Inspect the turbo.json configuration to ensure that inputs, outputs, and dependencies are correctly specified. When remote caching issues occur, verify authentication credentials and network connectivity to the cache provider.
# Debug task execution
turbo run build --dry-run
# Verbose output for troubleshooting
turbo run build --verbose
# Force rebuild all tasks
turbo run build --force
# Check cache contents
ls -la .turbo/cache/
# Validate turbo.json schema
turbo run build --graphCommon Error Messages
| Error | Cause | Solution |
|---|---|---|
| "Task not found" | Task not defined in turbo.json | Add task definition to turbo.json |
| "Circular dependency" | Tasks depend on each other cyclically | Restructure task dependencies |
| "Cache miss" | Inputs changed or cache cleared | Verify inputs configuration |
| "Remote cache auth failed" | Invalid or expired credentials | Re-run npx turbo link |
| "Package not found" | Workspace dependency not installed | Run npm install or yarn install |
Best Practices
- Define clear task dependencies — Use
^for dependency ordering - Specify outputs precisely — Only cache what's needed
- Use remote caching — Share cache across CI/CD and team
- Filter aggressively — Only build/test what changed
- Keep tasks focused — One responsibility per task
- Use workspace dependencies —
workspace:*for internal packages - Share configurations — Create shared tsconfig, eslint configs
- Monitor cache hit rates — Track how effective caching is
- Use dry run mode — Preview tasks before execution
- List all environment variables — Ensure correct cache invalidation
Common Pitfalls
| Pitfall | Impact | Solution |
|---|---|---|
| Missing outputs | No caching | Specify all build outputs |
| Wrong inputs | Cache misses | Include all relevant source files |
| No remote cache | Slow CI/CD | Set up Vercel or custom remote cache |
| Circular dependencies | Build failures | Restructure packages |
| Env variables not listed | Cache invalidation | List all env vars in turbo.json |
| Over-caching | Stale outputs | Exclude generated files from inputs |
| Under-caching | Slow rebuilds | Include all relevant source files in inputs |
| No task dependencies | Race conditions | Define explicit dependsOn relationships |
Conclusion
Turborepo 2.0 is a powerful build system that makes monorepo development fast and efficient. Its intelligent caching, parallel execution, and remote cache sharing eliminate redundant work across development and CI/CD.
The key insight is that most build work is repeated — the same code built multiple times across different machines. Turborepo's caching eliminates this waste, reducing build times from minutes to seconds for unchanged packages.
For teams managing monorepos with more than 10 packages, Turborepo's benefits become significant. A monorepo with 50 packages that takes 3 minutes to build from scratch can rebuild in 5 seconds when cached. Combined with remote caching, CI pipelines that previously took 12 minutes can complete in under 30 seconds for incremental changes. These time savings compound across the team, saving hundreds of engineering hours per month.
Key takeaways:
- Intelligent caching — Only rebuild what changed
- Parallel execution — Maximize CPU utilization
- Remote cache sharing — Share cache across team and CI/CD
- Task pipeline — Define dependencies between tasks
- Workspace support — Native monorepo package management
- Simple configuration —
turbo.jsonfor all settings - Filtering — Build only affected packages
- CI/CD integration — GitHub Actions, Vercel, and more