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.
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