Turborepo
The problem Turborepo solves is narrow and expensive: in a monorepo with twenty packages, npm run build rebuilds all twenty every time, even though you changed one line in one of them. Multiply that by every commit, every CI run, and every engineer, and a large fraction of your compute and your waiting is spent recomputing identical outputs.
Turborepo is a task orchestrator with a content-addressed cache. It reads a task graph from your configuration, works out which packages a change actually affects, runs only those tasks — in parallel, respecting dependency order — and replays cached output for everything else. A cache hit is effectively instant, and because the cache key is a hash of the inputs, hits are safe by construction rather than by heuristic.
The feature that changes team economics is remote caching. When CI builds a package, it uploads the result; when an engineer pulls that commit, they download it instead of rebuilding. Work is done once per unique input across the whole organization, not once per machine.
TL;DR
- Turborepo orchestrates tasks across packages, running only what a change affects.
- The cache key is a hash of inputs — source files, dependencies, environment variables, and the task config.
- Remote caching shares results across CI and every developer machine. Build once, globally.
dependsOn: ["^build"]means "build my dependencies first" — the^is the topological marker.- Declare
outputscorrectly or nothing gets cached; declareinputsto narrow what invalidates. envmust list every environment variable a task reads, or you'll get wrong cache hits.turbo pruneproduces a minimal subtree for small, well-layered Docker builds.- It does not manage dependencies — that's your package manager's workspaces feature.
Quick Example
The three cache hits took 31 milliseconds combined instead of rebuilding. On a wider repo with remote caching warm from CI, most runs are entirely cache hits.
Core Concepts
The task graph
dependsOn has two forms, and confusing them is the most common configuration mistake:
Caching
Because the key includes the environment variables the task reads, correctness depends on declaring them. A build that embeds NEXT_PUBLIC_API_URL but doesn't list it in env will serve a staging-built artifact for a production build — a wrong cache hit, which is the worst failure mode a build cache has.
Turborepo v2 has a strict environment mode that makes only declared variables visible to tasks, which turns this class of bug into an immediate failure instead of a silent one. Use it.
Remote caching
signature: true enables HMAC signing of cache artifacts with TURBO_REMOTE_CACHE_SIGNATURE_KEY, so a compromised cache can't inject arbitrary build output. On a shared cache this matters — an unsigned remote cache is a supply-chain surface.
For CI, use a read-write token on trunk builds and consider read-only for pull requests from forks, so untrusted code can't poison the cache other builds consume.
Pruned Docker builds
turbo prune is the feature that makes monorepo Docker images practical:
The --docker flag splitting manifests from source is the key detail: it lets the dependency-install layer cache independently of source changes, so editing a component doesn't reinstall node_modules.
Filtering
--filter='...[origin/main]' is the CI workhorse: build and test exactly what a pull request could have broken, and nothing else.
Turborepo vs. the Alternatives
Turborepo when you want caching and orchestration with minimal configuration in a JavaScript monorepo. Nx when you want generators, dependency-graph visualization, distributed task execution, and richer tooling integration — it does more and asks more. Bazel when you have a polyglot repo at real scale and need genuine hermeticity; it's correct and it's a significant organizational commitment.
Note that Turborepo is a task runner, not a package manager. Workspace linking and dependency resolution come from pnpm, npm, yarn, or bun workspaces — Turborepo sits on top. See Package Managers and Monorepos.
Best Practices
Declare outputs precisely, and remember the exclusions
An empty or missing outputs means nothing is restored on a cache hit, so a "hit" produces no artifact and the next task fails confusingly. Equally, over-broad outputs cache junk: ".next/" without "!.next/cache/" stores Next.js's own incremental cache, bloating artifacts substantially.
List every environment variable a task reads
Missing env entries cause wrong cache hits — the most damaging cache bug, because it's silent and produces artifacts built with the wrong configuration. Enable strict environment mode so undeclared variables are invisible rather than merely unhashed.
Narrow inputs so unrelated changes don't invalidate
By default, any non-gitignored file change in a package invalidates its tasks. Listing inputs: ["src/**", "package.json"] means editing the README doesn't rebuild. On a large repo this materially raises the hit rate.
Set up remote caching on day one
It's the feature that pays for the setup, and it's a token plus one command. Without it, every machine rebuilds everything independently and you've bought only local caching, which CI never benefits from.
Sign remote cache artifacts
signature: true plus a signing key. A shared cache that anyone with write access can poison is a supply-chain vulnerability, and pull requests from forks are the obvious vector.
Use --filter='...[origin/main]' in CI
Build and test only what the change could affect, plus everything downstream of it. This is where the largest CI time savings come from — larger than caching alone on a wide repo.
Mark dev servers cache: false and persistent: true
A long-running task that Turborepo waits on will hang the run, and caching a dev server's output is meaningless. Both flags together are required for watch-mode tasks.
Inspect the graph when caching misbehaves
turbo build --dry-run=json shows the resolved task graph, the computed cache key, and the hashed inputs for each task. When a task refuses to hit cache, diffing that output between two runs identifies which input changed.
Common Mistakes
Missing or wrong outputs
Undeclared environment variables
Confusing ^build with build
Caching a dev server
Copying the whole monorepo into Docker
Building everything in CI
FAQ
Turborepo or Nx?
Turborepo if you want caching and task orchestration with almost no configuration and your repo is JavaScript or TypeScript. Nx if you want more: code generators, a queryable dependency graph, distributed task execution across CI agents, and deeper framework integrations. Nx does considerably more and has a correspondingly larger surface to learn. Both are good; the deciding factor is usually whether you want a tool or a platform.
Does it replace my package manager?
No. Workspace linking, dependency resolution, and installation come from pnpm, npm, yarn, or bun workspaces. Turborepo reads that workspace configuration to derive the package graph and then orchestrates tasks over it. You need both.
Is remote caching safe?
With signing enabled, yes for the integrity property — artifacts are HMAC-verified, so a tampered cache entry is rejected. The remaining considerations are access control (who can write) and secret leakage (a build that writes a secret into its output caches that secret). Use read-only tokens for untrusted pull requests, and don't let build outputs contain credentials.
Can I self-host the cache?
Yes. The remote cache API is a documented HTTP spec, and several open-source implementations exist (turborepo-remote-cache being the most widely used), deployable to your own infrastructure or object storage. Vercel's hosted cache is the zero-setup option and is not a requirement.
Why does my task never hit the cache?
Run turbo build --dry-run=json on two runs and diff the hashes. The usual causes: a file in the default input set changes every run (a generated timestamp, a lockfile churn, a .tsbuildinfo not in outputs), an environment variable differs between machines, or the task writes into its own input directory. Narrowing inputs fixes most of them.
Does it work outside JavaScript?
It will run arbitrary commands, so you can orchestrate a Go or Python task in a JS-managed monorepo. What it can't do is understand non-JS dependency graphs — it derives the package graph from package.json workspaces. For a genuinely polyglot repo at scale, Bazel or Pants is the better fit.
Related Topics
- Monorepos — The structure Turborepo exists to make fast
- Package Managers — Workspaces, which Turborepo builds on
- Build Tools — Bundlers and compilers Turborepo orchestrates
- Linting & Formatting — A common cached task
- CI/CD — Where filtering and remote caching pay off most
- Docker — Pruned multi-stage builds
- Vite · esbuild — The underlying build steps
- Next.js — The most common Turborepo app target