AIO APEX
fr

Turborepo, Nx, and Bazel: how monorepo build caching actually differs in 2026

Partager:
Turborepo, Nx, and Bazel: how monorepo build caching actually differs in 2026

Every monorepo build tool sells the same pitch: hash the inputs to a task, skip the task if that hash was already built, and share the cache across the team so nobody rebuilds what a teammate already built ten minutes ago. Turborepo, Nx, and Bazel all do this. The thesis of this piece is that the pitch is the easy 20 percent — the real differences between these tools show up in cache correctness, remote execution, and what happens when a repo grows past a few hundred packages, and those differences should drive the choice far more than the marketing comparison tables do.

Turborepo: fast to adopt, narrower guarantees

Turborepo's appeal is that it bolts onto an existing JavaScript or TypeScript monorepo in an afternoon. You define a turbo.json pipeline describing task dependencies (build depends on lint, test depends on build), and Turborepo content-hashes each task's inputs — source files, environment variables you explicitly declare, and the outputs of upstream tasks — to decide whether to run or restore from cache.

The catch is in that word "explicitly." Turborepo's cache correctness depends on you declaring every input that affects a task's output, including environment variables. Miss one — a feature flag, an API base URL baked in at build time — and Turborepo will confidently restore a stale cached build that doesn't reflect the actual output for that environment variable's new value. This isn't a bug; it's the tradeoff for a lightweight tool that doesn't sandbox builds. Teams that adopt Turborepo and then get bitten by this usually respond by over-declaring inputs, which quietly erodes the cache hit rate that was the entire point of adopting it.

Nx: a dependency graph you can actually query, plus the same caching tradeoff

Nx shares Turborepo's content-hash caching model — same strength, same explicit-input weakness — but adds a feature that matters more as a repo grows: a real, queryable project graph. nx graph and nx affected let you ask "which projects does this change actually touch" and run tasks only for the affected subset, which Turborepo can approximate through its own dependency declarations but doesn't expose as a first-class, inspectable graph in the same way.

That matters concretely at CI scale: a monorepo with 150 packages where a single PR touches two of them shouldn't trigger a full-repo test run. Nx's affected-graph model, combined with distributed task execution across CI agents, is built around that exact problem. The honest comparison point is that Nx's caching correctness has the same environment-variable footgun as Turborepo — it's the same underlying trust model, just with a better UI for understanding what's in your dependency graph.

Bazel: hermetic builds solve the correctness problem, at a real cost

Bazel takes a fundamentally different approach, and it's the one that actually solves the cache-correctness problem rather than mitigating it. Bazel builds run in a sandbox with no access to the ambient filesystem or environment beyond what's explicitly declared as an input — a Bazel build genuinely cannot silently depend on an environment variable you forgot to declare, because the sandbox doesn't expose it. This is what Bazel calls a hermetic build, and it's why Google, which open-sourced Bazel from its internal Blaze system, trusts build caching at a scale of billions of lines of code across thousands of engineers.

The cost is adoption friction that Turborepo and Nx deliberately avoided. Bazel requires describing your build graph in its own BUILD file format (Starlark, a Python dialect), which means wrapping existing npm, pip, or Maven dependencies through Bazel-specific rules (rules_js, rules_python, rules_jvm_external) rather than just pointing Bazel at your existing package.json or requirements.txt. Migrating an existing 100-package JavaScript monorepo to Bazel is routinely a multi-month project, not an afternoon. Teams that make this investment are usually ones where correctness failures from a bad cache hit have already cost them — a shipped build that silently used stale code — or where build scale (thousands of targets, remote execution across a build farm) has outgrown what a non-hermetic tool can guarantee.

Picking between them

The decision isn't really "which tool is fastest" — all three are fast enough for a cache hit to feel instant. It's a question of what failure mode you can tolerate and how much migration pain you can absorb:

  • Pick Turborepo if you have an existing JS/TS monorepo under roughly 50 packages, want caching running by end of week, and are willing to own the discipline of declaring every input explicitly (and auditing it when something feels stale).
  • Pick Nx if you're past that size, need the affected-graph model for sane CI times, and want generator/plugin tooling for scaffolding new packages consistently — accepting the same input-declaration discipline Turborepo requires.
  • Pick Bazel if you've already been burned by a stale-cache bug reaching production, you're polyglot (Java, Go, Python, and JS in one repo), or your CI scale genuinely needs remote execution across a build farm rather than just a shared remote cache — and you can staff the migration.

A useful gut check before committing: go find the last time your team shipped a build that silently used a stale artifact. If that's never happened, Turborepo or Nx's lighter-weight model is probably the right tradeoff. If it has happened more than once, that's Bazel's hermetic sandbox paying for its own migration cost.

Partager:
Turborepo vs Nx vs Bazel: Monorepo Caching Compared | IRCNF | AIO APEX