Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Compare the Nx approach

Nx vs Turborepo Performance Benchmarks

You want a defensible answer to "which task runner is faster for our monorepo," and you want it backed by numbers from your own hardware rather than a vendor landing page. This page lays out a reproducible benchmark protocol — cold and warm cache, identical runners, cleared state — explains why the two tools diverge on the same workload, and shows the configuration that makes each one's hashing deterministic so the comparison is fair. If you are still deciding which to adopt rather than measuring an existing pick, start from Choosing a Monorepo Task Runner; this page assumes you want to put both on a stopwatch.

Symptoms that trigger a benchmark

Teams reach for a head-to-head measurement when CI exhibits:

• cache hit ratio: 54%  (expected > 90% on an unchanged PR)
• Task graph computation took 3.8s before any task ran
• repeated full rebuilds despite no source changes in 4 of 6 packages
• wall-clock build time varies ±40% run to run on identical input

These point at task-graph or cache-invalidation behavior, not raw compiler speed — which is exactly where Nx and Turborepo differ.

Root cause analysis

The two tools answer "what changed and what must re-run" differently. Nx builds a computed project graph from explicit project configuration and filesystem analysis, then invalidates tasks based on that graph and namedInputs. Turborepo uses a lighter content-addressable file-hashing engine in its Go binary, hashing the inputs globs you declare in your Turborepo Pipeline Configuration. When cache invalidation misaligns with real workspace boundaries — over-broad inputs, untracked transitive edges — either tool does redundant work, serializes tasks that could parallelize, or over-fetches unrelated packages. Sound measurement is part of designing a resilient Monorepo Architecture & Orchestration setup, so the protocol below controls for every variable that moves the numbers.

Cold vs warm build times Bar chart comparing illustrative cold-cache and warm-cache build durations for Nx and Turborepo. cold warm scenario Turborepo cold ~ 92s Nx cold ~ 101s Turborepo warm ~ 7s Nx warm ~ 5s bar length = wall-clock seconds (illustrative)
Illustrative shape only: cold builds are close, warm builds collapse to single-digit seconds once the cache holds — your own runs are what matter.

Resolution: a reproducible benchmark protocol

Run every measurement on identical CI runners with state fully cleared between cycles. The numbers are meaningless otherwise.

1. Clear all caches

rm -rf .turbo node_modules/.cache .nx/cache

2. Measure cold-cache builds

# Nx cold: skip the cache entirely
nx run-many --target=build --all --parallel=4 --skip-nx-cache

# Turborepo cold: force execution
turbo run build --concurrency=4 --force

3. Measure warm-cache builds and inspect hits

# Re-run with no source change; expect near-total hits
nx run-many --target=build --all --parallel=4 --verbose

turbo run build --concurrency=4 --dry=json \
  | jq '.tasks[] | {id: .taskId, cache: .cache.status}'

Capture wall-clock time, peak CPU/RAM, and hit ratio for each of the four cells (Nx/Turborepo × cold/warm). Watch the verbose and dry-run output for serialized tasks or over-fetching of unrelated packages, and tighten dependency declarations to remove false-positive invalidation.

Deterministic configuration patches

Both tools need strict input/output boundaries before a comparison is fair; otherwise an unrelated config edit busts one tool's cache and not the other's.

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"],
      "inputs": ["src/**", "package.json", "tsconfig.json"]
    }
  }
}
// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
  }
}

Scoping namedInputs and inputs this way stops an unrelated root-config change from triggering a full workspace rebuild in either tool.

What the four numbers actually tell you

A benchmark produces four cells; each answers a different question, and reading them together is what turns raw seconds into a decision.

Cell What it measures What a bad number means
Turborepo cold Raw orchestration + compile overhead A slow cold run points at task serialization, not caching
Nx cold Same, plus project-graph computation time A large gap vs. Turborepo cold is graph-construction cost
Turborepo warm Restore speed from cache on an unchanged graph A warm run far above single digits means misses, i.e. hash drift
Nx warm Same, via the computed graph High warm time with low hit count means over-broad namedInputs

The cold numbers are usually close because both tools ultimately shell out to the same compilers; the interesting signal is in the warm column and the hit ratio. A warm run that is not near-instant is not a speed problem — it is a correctness problem in your inputs/namedInputs, and chasing it as "make caching faster" wastes effort. Fix determinism first, then compare warm times.

A second subtlety: Nx pays a fixed graph-computation cost on every invocation, which dominates on tiny repos and disappears on large ones, while Turborepo's per-task hashing scales with the number of files. On a repo with few packages but many files per package, Turborepo's hashing can cost more than Nx's graph walk; on a repo with many small packages, the reverse holds. This is why "which is faster" has no repo-independent answer and why the protocol insists on measuring your own tree.

Eliminating measurement noise

Three sources of variance will wreck an otherwise careful benchmark. Disk cache warmth: the OS page cache makes a second cold run faster than the first, so run each cold measurement on a fresh runner or sync && echo 3 > /proc/sys/vm/drop_caches between cycles where you control the host. Background load: a CI runner sharing a host with other jobs adds jitter, so prefer dedicated runners for the comparison and report the median of at least five cycles rather than a single value. Network variability: remote-cache download time depends on the link, so isolate the orchestration comparison by benchmarking with only the local cache first, then add the remote layer as a separate measurement.

# Median of five warm runs, Turborepo, local cache only
for i in $(seq 1 5); do
  /usr/bin/time -f "%e" turbo run build --concurrency=4 2>> turbo_warm.txt
done
sort -n turbo_warm.txt | awk '{a[NR]=$1} END{print "median:", a[int(NR/2)+1]}'

CLI validation

# Confirm warm runs are actually hitting cache, not silently re-running
turbo run build --dry=json | jq '[.tasks[] | select(.cache.status=="HIT")] | length'
nx run-many --target=build --all --parallel=4 --verbose | grep -c "from cache"

Prevention and CI guardrails

  • Pin package.json and the lockfile so transitive drift cannot alter content hashes mid-benchmark.
  • Set identical remote-cache TTLs (for example 14 days on feature branches) for both tools so storage policy is not a hidden variable.
  • Document hardware specs, concurrency, and the cache-clear procedure in a runbook; re-run before merging any toolchain upgrade.
  • Use --graph or --dry output to catch implicit dependency cycles that force sequential execution and skew timings.

Frequently Asked Questions

Which tool delivers faster cold build times for large TypeScript monorepos? They are usually close, with Turborepo's Go binary and file-level hashing giving it a small edge on cold runs; Nx can pull ahead in highly interconnected workspaces where its project-graph inference avoids re-running tasks that Turborepo's coarser hashing would repeat. Measure your own graph rather than trusting a general ranking.

How do cache invalidation strategies differ between Nx and Turborepo? Nx invalidates from a computed project graph with explicit dependency tracking; Turborepo invalidates from content-addressable hashing of declared inputs. Turborepo's model is easier to reason about for a single library, while Nx offers finer control over workspace boundaries in deeply connected repos.

What concurrency setting makes a benchmark fair? Match the runner's CPU core count — --concurrency=4 for Turborepo and --parallel=4 for Nx on a 4-core runner — and hold it constant across every cell, alongside identical package versions and cleared caches, so the only variable is the tool.

Related

Turborepo Pipeline Configuration