What is Turborepo?
Turborepo is a high-performance build system for JavaScript monorepos. It runs tasks across all the packages in your repository, respects the dependency order between them, and caches results so that unchanged work is never repeated.
A monorepo without a build system quickly becomes painful. Running build and test in every package by hand is slow, and a naive script re-runs everything even when only one package changed. Turborepo solves both: a declarative task pipeline handles ordering, and content-based hashing makes unchanged tasks effectively free.
The task pipeline
The pipeline lives in turbo.json. Each task declares its dependencies and outputs.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
dependsOn with a caret (^build) means “build my dependencies first”. Without the caret, it refers to a task in the same package. outputs tells Turborepo what to cache, and cache: false opts a task like dev out because it runs indefinitely.
Running tasks
One command runs a task across the whole repository, in the correct order and in parallel where possible.
turbo run build
turbo run test lint
turbo run dev --filter=web
Turborepo builds a graph from dependsOn, runs independent tasks concurrently and queues dependent ones. The result is the fastest possible schedule that still respects the constraints between packages.
Caching
Caching is the feature that changes how a monorepo feels. For each task, Turborepo hashes the inputs — source files, dependencies, environment variables and configuration — and stores the outputs and logs against that hash.
On the next run, if the hash is unchanged, the task is skipped and its outputs are restored from cache. The logs are replayed too, so the output looks the same without the work being done. On a warm cache, a full turbo run build across dozens of packages can finish in seconds.
Remote caching
A local cache only helps one machine. Remote caching shares the cache across the team and CI.
- One developer builds a package; the result is uploaded.
- Another developer checks out the same commit and restores it instantly.
- CI restores the same artifacts, skipping work it has already done elsewhere.
This turns the cache into a shared asset and is often the single biggest CI speedup for a monorepo. Vercel offers a hosted remote cache, and self-hosted options exist for teams that need them.
Filtering
Large monorepos rarely need to run every task. Filtering targets a subset of packages.
# only packages affected by changes since main
turbo run test --filter="...[origin/main]"
# one package and its dependencies
turbo run build --filter=web...
# only packages that depend on @repo/ui
turbo run build --filter=...@repo/ui
The [origin/main] syntax asks Turborepo to compute which packages changed and include their dependents. In CI this means a pull request touching one package only builds and tests what it can actually affect.
Workspaces and structure
Turborepo does not manage dependencies itself — that is the package manager’s job. You define workspaces with pnpm, npm, yarn or bun, and Turborepo layers the pipeline and cache on top.
repo/
├── apps/
│ ├── web/ # a deployable app
│ └── docs/
├── packages/
│ ├── ui/ # a shared component library
│ └── config/ # shared config
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
Apps consume shared packages through the workspace protocol, and Turborepo understands that dependency graph when it orders tasks. The combination of pnpm workspaces for linking and Turborepo for task running is the most common modern monorepo setup.
Best practices
- Declare
outputsfor every cacheable task. - Use
dependsOnwith^to express dependency order. - Mark long-running tasks such as
devwithcache: false. - Enable remote caching in CI for the largest gains.
- Filter by changed packages in CI to keep pipelines fast.
- Keep
turbo.jsonat the repository root and share it across packages. - Add a root script so the whole team runs the same commands.
Common mistakes
- Forgetting
outputsand losing the cache benefit. - Running tasks in an arbitrary order instead of using
dependsOn. - Caching a persistent task like a dev server.
- Including volatile environment variables in the hash unnecessarily.
- Running the full pipeline in CI when filtering would skip unaffected packages.
- Treating Turborepo as a replacement for a package manager.
Where to go next
Turborepo turns a monorepo from a burden into an advantage. Pair it with pnpm for workspace linking, understand the npm and Node.js foundations, and keep individual packages building with Vite. Then add a root turbo run build to a repository with more than one package and watch the second run finish almost instantly.