The Build System Wars — Bazel, Buck2, Pants, and Whether Your Monorepo Actually Needs One

Every engineering org with a monorepo eventually reaches the same inflection point: builds are slow, CI is expensive, and someone on the team reads a blog post about how Google builds everything in seconds with Bazel. The conversation starts. “Should we adopt Bazel?” The promise is genuinely compelling — hermetic builds, massive parallelism, remote caching, and incremental builds that only recompile what actually changed. Who wouldn’t want that?

But here’s what the blog posts don’t tell you: build system migrations are among the most painful infrastructure projects a team can undertake. I’ve led two of them now, and I want to share an honest assessment of the current landscape so others can avoid the mistakes we made.

Our Evaluation

We evaluated Bazel, Buck2, and Pants for our 200K LOC TypeScript/Python monorepo with 15 engineers. Here’s what we found.

Bazel: The Gold Standard (With a Gold-Plated Learning Curve)

Bazel is the tool everyone benchmarks against. Used by Google, Stripe, Uber, and dozens of other companies operating at serious scale. The build model is elegant — everything is a target with declared inputs and outputs, builds are hermetic (no implicit dependencies on system state), and the remote execution API means you can distribute builds across a cluster of workers.

But the learning curve is brutal. BUILD files feel like learning a new language (because you are — Starlark). The mental model shift from “run this script” to “declare this target’s dependencies” is non-trivial. Our engineers, who are productive TypeScript and Python developers, struggled with BUILD file syntax for weeks. The community has grown significantly, but documentation still assumes Google-scale problems. When you’re debugging why ts_project isn’t picking up your path aliases, Stack Overflow has three answers and two of them are outdated.

For our 15-person team, Bazel felt like bringing a tank to a knife fight. The power is undeniable, but the operational overhead was disproportionate to our actual build problems.

Buck2: Fast, Modern, and Risky

Meta’s ground-up rewrite of Buck in Rust is genuinely impressive from an engineering standpoint. It’s faster than Bazel in most benchmarks, offers better developer ergonomics with its Starlark dialect, and introduces real innovation in the build graph model — particularly around dynamic dependencies and incremental computation.

But the ecosystem is young. Community plugins are sparse, Stack Overflow answers are rare, and there’s a legitimate concern: Meta built this for Meta. If Meta deprioritizes Buck2 (and large companies have a track record of abandoning open-source projects when internal priorities shift), you’re on your own with a build system that has a small community. The bus factor for the entire project is essentially one company’s continued investment.

Pants: The Underdog That Delivers

Pants surprised us. It’s Python-first with excellent TypeScript support, and it has the friendliest developer experience of the three tools. Setting up Pants took 2 days versus 2 weeks for a comparable Bazel setup. The documentation is clear, the error messages are helpful (a low bar that Bazel still struggles with), and remote caching just works with their cloud offering — no need to set up and maintain your own remote cache infrastructure.

The trade-off: Pants doesn’t support as many languages as Bazel. If you have a polyglot monorepo with Go, Java, Rust, and C++ alongside your TypeScript and Python, Bazel is still the pragmatic choice. But for our TypeScript/Python stack, Pants covered everything we needed.

The Numbers

Here’s what our benchmarks showed across all three tools:

Metric npm/pip (baseline) Bazel Buck2 Pants
Clean build 8 min 12 min 9 min 10 min
Incremental build 8 min 25 sec 20 sec 28 sec
CI with remote cache N/A -62% -65% -60%
Setup time N/A 2 weeks 1 week 2 days

The clean build numbers are counterintuitive — Bazel is actually slower for a full build because of the overhead of its build graph analysis. But nobody does clean builds regularly. The incremental build is the real win: going from 8 minutes to under 30 seconds fundamentally changes how developers work. You save, you build, you see results immediately. The feedback loop tightens dramatically.

Remote caching reduced CI times by roughly 60% regardless of which tool we used — and that’s the most important takeaway from our entire evaluation.

The Controversial Conclusion

Most monorepos under 500K LOC don’t need Bazel. The complexity overhead — BUILD files, Starlark, remote execution infrastructure, the learning curve — doesn’t pay off until you have 50+ engineers and build times measured in tens of minutes. For teams under 20 engineers, Pants or even Turborepo (for JS/TS-only monorepos) gives you 80% of the benefit at 20% of the complexity.

We chose Pants. Six months in, we haven’t regretted it. Our CI times dropped 60%, incremental builds are near-instant, and the migration was completed without a quarter-long productivity hit.

Question

What build system does your monorepo use, and at what team size or codebase scale did you feel the need for a dedicated build tool? I’m curious whether our 500K LOC threshold resonates with others or if we’re being too conservative.

We went through this exact evaluation about 6 months ago, and I want to offer a strong endorsement for a tool you mentioned only briefly: Turborepo + pnpm workspaces.

For a JS/TS-only monorepo, this is the answer. Full stop. It took us an afternoon to set up — not two days, not two weeks, literally an afternoon. One turbo.json config file, a few pipeline definitions, and we were up and running. Remote caching was a single config line pointing to Vercel’s cache (free tier covers most teams). The incremental build performance is excellent, and the developer experience is seamless because it feels like a natural extension of the npm ecosystem rather than a completely new paradigm.

The key advantage Turborepo has over Bazel/Buck2/Pants for JS/TS teams is that you don’t need to change how you think about your code. There are no BUILD files. Your package.json files are your build definitions. Your existing npm scripts are your build commands. Turborepo just adds intelligent caching and task scheduling on top of what you already have. The migration effort is essentially zero — you add Turborepo to your existing monorepo and it starts making things faster immediately.

Our numbers: 150K LOC of TypeScript, 10-person team. Before Turborepo, CI took 22 minutes. After Turborepo with remote caching, CI takes 6 minutes on cache hits and 14 minutes on cache misses (still faster because of parallel task execution). Local incremental builds went from “rebuild everything” to sub-10-second rebuilds of affected packages.

I’ll be honest — we only considered Bazel because “that’s what serious companies use.” There was a real pull toward it just for the prestige factor. But for our scale and our tech stack, Turborepo solved our actual problems (slow CI, redundant builds, inefficient task ordering) without the learning curve tax.

The caveat: if we had Python and Go services in the same monorepo, the calculation would be completely different. Turborepo is JS/TS only. The moment you need polyglot support, you’re back to evaluating Bazel, Buck2, or Pants. But if your monorepo is JavaScript all the way down, don’t overthink this.

I want to highlight something your post touches on but doesn’t fully emphasize: the hidden cost of build system migrations is team productivity during the transition.

When we adopted Bazel about 18 months ago, our team spent 3 months in a significantly degraded state. And I don’t mean “slightly slower.” I mean every other PR had BUILD file issues. Developers who had been productive for years were suddenly struggling with cryptic Starlark errors. Our build infrastructure team — 2 people — was drowning in support requests, doing nothing but writing BUILD files for other engineers and debugging dependency resolution issues.

The productivity dip was real and measurable. Feature velocity dropped 25% during the migration quarter. That’s not a subjective feeling — we tracked it in our sprint metrics. Fewer stories completed, more time in code review arguing about BUILD file organization, and a general sense of frustration across the team. Two engineers told me in 1:1s that they were considering leaving because “the build system migration is making their daily work miserable.”

If I could redo it, here’s what I’d change:

  1. Budget 6 months for the migration, not 3. We rushed it because leadership wanted the CI improvements by end of quarter. The compressed timeline meant we cut corners on documentation and training, which made the learning curve even steeper.

  2. Dedicate build infrastructure engineers full-time during the migration. Our 2 infra engineers were also responsible for maintaining existing CI/CD, responding to incidents, and handling other infrastructure work. They were spread too thin.

  3. Communicate clearly to leadership that feature velocity would temporarily decrease. We didn’t set this expectation, and when the productivity numbers came in, there was an uncomfortable conversation about whether the migration was “worth it.”

  4. Run old and new build systems in parallel for at least a month. We did a hard cutover and it created chaos.

The long-term payoff was genuinely worth it — CI times dropped 70%, and developers now love the incremental build speed. But the short-term cost nearly killed the project politically. We were one bad quarter away from leadership pulling the plug and reverting to npm scripts.

Your Pants evaluation showing a 2-day setup time is incredibly appealing from a change management perspective. If I could get 80% of Bazel’s benefits with 20% of the migration pain, that’s an easy decision for any engineering leader.

I want to push back on the framing slightly. Before anyone invests weeks into evaluating Bazel vs. Buck2 vs. Pants, ask one simple question: “Do we have remote caching?”

If the answer is no, stop everything and add remote caching to your existing build system. This is the 80/20 of build optimization. It’s the single highest-ROI change you can make to your build infrastructure, and it requires zero migration risk.

We added remote caching to our existing Gradle build using a self-hosted cache backed by S3. It took one engineer two days to set up. CI times dropped 55% immediately — not because our builds got smarter, but because most CI runs are rebuilding artifacts that another CI run (or another developer) already built five minutes ago. The cache hit rate after the first week was 73%, and it’s been climbing as more builds populate the cache.

For npm/yarn builds, you can achieve similar results with:

  • Nx Cloud — remote caching for Nx workspaces, generous free tier
  • Turborepo — as alex_dev mentioned, remote caching is built in
  • Custom S3 caching — use content-addressed hashing of your build inputs as cache keys, store artifacts in S3, pull from cache before building

For Python, pip’s wheel cache combined with a shared artifact store gives you most of the benefit. For Docker builds, BuildKit’s cache export/import with a shared registry is transformative.

The point is: remote caching is orthogonal to your build tool choice. You can bolt it onto npm, pip, Gradle, Maven, or any other build system. And it will give you the majority of the speed improvement that people attribute to “adopting Bazel.”

Only after you’ve exhausted the remote caching approach should you evaluate whether the additional benefits of a dedicated build tool — the incremental build graph, hermetic builds, language-agnostic support, and remote execution — are worth the migration effort. For many teams, they won’t be. The incremental build graph matters most for very large codebases where the build graph is deep and wide. Hermetic builds matter most when you’re debugging “works on my machine” issues frequently. Remote execution matters most when single-machine build times exceed what remote caching can solve.

For most teams under 30 engineers, remote caching is the answer, not a build system rewrite.