Turborepo vs Nx: Monorepo Build Tool Comparison
You are choosing a build orchestrator for a JavaScript or full-stack monorepo and need to know how Turborepo and Nx actually differ for incremental builds and affected detection — task graph, remote caching, tooling scope, and migration cost — not just which has more stars.
When to use each — the short version
- Turborepo when you want fast, low-ceremony task orchestration and caching layered onto an existing JS workspace with minimal new conventions.
- Nx when you have a large or multi-language monorepo and want a richer task graph, code generators, module-boundary enforcement, and a plugin ecosystem — and will invest in its conventions.
Prerequisites
The comparison at a glance
| Dimension | Turborepo | Nx |
|---|---|---|
| Core model | Task pipeline over workspace scripts | Project graph with executors/targets |
| Affected detection | --filter=...[base] git range |
nx affected from a base ref |
| Config surface | Small turbo.json |
nx.json + project configs + plugins |
| Remote caching | Built-in, content-addressed | Built-in (Nx Replay), content-addressed |
| Code generation | None (bring your own) | First-class generators/schematics |
| Language scope | JS/TS-focused | JS/TS + plugins for many ecosystems |
| Module boundaries | Not enforced | Lint rule enforces allowed imports |
| Adoption cost | Low | Moderate to high |
Equivalent commands side by side
Building only what a PR changed looks like this in each tool.
# Turborepo — build packages affected since the merge base, with remote cache
pnpm turbo run build --filter='...[origin/main]' --remote-only
# Nx — build projects affected since the merge base
pnpm nx affected --target=build --base=origin/main// Turborepo — turbo.json: declare the task graph and cache inputs/outputs
{
"tasks": {
"build": {
"dependsOn": ["^build"], // build deps first
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**"] // what to cache
}
}
}// Nx — project.json: targets and cacheable operations
{
"targets": {
"build": {
"executor": "@nx/vite:build",
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
}
}
}Step-by-step walkthrough
Task graph and affected detection. Both compute a dependency graph and run only the tasks impacted by changed files. Turborepo derives affected scope from a git range in --filter; Nx has a dedicated affected command over its project graph. The affected-detection mechanics are conceptually the same — only build what a change touches.
Caching. Each tool content-addresses task outputs, so an unchanged package’s dist/ is restored instead of rebuilt, locally and across runners via remote caching. Steady-state CI speed is therefore comparable; the win over a naive build is large for both.
Config surface and scope. Turborepo’s turbo.json is small and declarative, layering onto scripts you already have. Nx models projects, targets, and executors, adding generators, module-boundary lint rules, and plugins for non-JS ecosystems — more capability in exchange for more convention.
Migration cost. Adopting Turborepo is typically a turbo.json plus a few script tweaks. Adopting Nx is a larger commitment, especially if you take its generators and enforced boundaries; the payoff is stronger for very large or polyglot repos.
Verification
# Prove caching works on either tool: run twice with no changes; the second run is a full cache hit.
pnpm turbo run build # first: builds; second: ">>> FULL TURBO" (all cached)
pnpm nx run-many -t build # first: builds; second: "Nx read the output from the cache"
# Prove affected scope: touch one package and confirm only its build runs.
touch packages/ui/src/x.ts
pnpm turbo run build --filter='...[HEAD^]' # only ui + dependents rebuildExpected: repeat runs are cache hits, and an isolated change rebuilds only the affected projects.
Common pitfalls
- Wrong cache
outputs/inputs. If a task’s declared outputs miss a produced file, the cache restores an incomplete artifact; if inputs miss a source glob, it serves stale output. Declare both precisely, per deterministic cache keys. - Choosing on benchmarks alone. Steady-state speed is similar; decide on scope, language mix, and how much convention your team will adopt, not a synthetic build-time chart.
- Skipping remote cache in CI. Without a shared remote cache, ephemeral runners rebuild everything each time. Enable it on whichever tool you pick to get the cross-machine reuse.
What the comparison usually gets wrong
Most comparisons of these two tools list features, and the feature lists have converged to the point where the list is no longer the deciding factor. Both compute a task graph, both cache task outputs locally and remotely, both determine an affected set from a git range, and both parallelise across available cores. Choosing on those axes produces a coin flip.
The decision that actually matters is how much opinion you want the tool to hold about your repository. Turborepo takes the repository as it finds it: a turbo.json describing task dependencies, layered over whatever workspace layout already exists. Nx has a model of what a workspace should look like — projects with declared types, tags describing what may depend on what, generators that create new projects in the sanctioned shape — and delivers more once the repository conforms to it.
The migration cost is the real number
In a greenfield repository both are roughly a day’s work and Nx’s additional capabilities are close to free. In an existing repository with its own conventions, adopting Nx means either conforming to its project model or configuring around it, and that is measured in weeks rather than days. Adopting Turborepo in the same repository is an afternoon, because it asks almost nothing of the existing structure.
That asymmetry is why the same comparison reaches different conclusions for different teams without either being wrong. A team starting fresh, or one already frustrated by cross-package imports that nothing prevents, gets more from the heavier tool. A team with a working monorepo that simply wants to stop rebuilding everything gets the same caching benefit for a fraction of the effort.
Signals that point to the heavier tool
Three situations genuinely justify the larger investment. Boundaries that keep leaking — packages importing each other by relative path, or a shared library reaching into an application — are enforceable in Nx and merely discouraged elsewhere; if that leakage is a recurring source of breakage, the enforcement pays for the migration. Repositories with many similar projects benefit from generators, because a consistent project shape stops being a review responsibility. And a large graph is easier to reason about with a visualiser than with a config file, once the package count passes roughly thirty.
Absent those signals, the lighter tool is the better default, and both choices remain reversible. Neither is architecture: both are configuration plus a cache backend, and moving between them is a matter of days rather than a rewrite. That reversibility is worth stating explicitly, because these decisions attract more debate than their cost of being wrong justifies.
Whichever you pick, measure the affected-set distribution first. A repository whose changes are consistently wide gains little from either tool, and the effort is better spent on the coupling that makes every change wide in the first place.
Related
- Incremental Builds and Affected Detection in Monorepos — the parent guide on the shared affected-build model.
- How to Configure Nx Affected Commands for Faster PR Checks — the Nx-specific affected workflow.
- Implementing Remote Build Caching with Turborepo — the Turborepo remote cache in depth.
- Build Optimization & Caching Strategies — the section overview.
← Back to Incremental Builds and Affected Detection in Monorepos