Bazel and Buck2 Diverge on How Build Graphs Age
Bazel and Buck2 disagree about whether a build graph should be preserved or discarded. That disagreement shapes what monorepo teams maintain, how they staff build engineering, and how much they pay for a clean checkout. The split is structural, not stylistic, and it shows up years into a migration.
The Build Graph as a Living Artifact
Bazel treats the build graph as an asset worth protecting. Target definitions, action caches, and remote execution state accumulate value as a repository grows. The longer the graph lives, the more useful incrementality becomes. That premise holds at moderate scale and starts to strain when a monorepo crosses into tens of thousands of targets.
Buck2, developed at Meta and released as open source, inverts the premise. The graph is disposable. Rebuilding it from scratch is cheap enough that preserving it is not a goal. Buck2 supports C++, Swift, Java, Kotlin, Python, Rust, Go, and more, and it was designed for a monorepo where the graph is regenerated constantly.
Both systems read BUILD files and both produce dependency graphs. The difference is what each expects the graph to become after a few years. Bazel expects it to mature. Buck2 expects it to be replaced. Age is a design constraint in both, but the constraint points in opposite directions.
How Bazel Accumulates Structural Debt
Incremental builds degrade over years. A repository that built in minutes in 2021 can take substantially longer by 2025 without any single change explaining the slowdown. Stale targets accumulate in WORKSPACE files, unused dependencies linger, and the dependency graph develops long chains that nobody remembers adding.
Remote cache invalidation grows costly as the graph widens. A change to a widely imported library can invalidate a large fraction of cached actions, and the rebuild that follows is proportional to how tangled the graph has become. Teams respond by adding cache layers and tuning action keys, which adds configuration surface.
Engineers learn to distrust green builds. When a passing CI run does not guarantee a passing clean build, developers start running local full builds before merging. That habit is rational and expensive. It also signals that the graph has stopped being a reliable contract and become a negotiation.
The debt is not uniform. It concentrates in directories that change rarely and are owned by teams that have moved on. A shared utility library that once served three services may still be imported by a dozen targets long after its original consumers were deleted. Each of those imports keeps the library alive in the graph, and each one adds a node that must be considered during invalidation. Over time, the graph accumulates a penumbra of low-value targets that slow every operation without contributing to any current product.
Buck2's Bet on Ephemeral Graphs
Buck2 was written to make graph regeneration cheap. The engine is faster than its predecessor, and the design assumes that throwing away the graph and rebuilding is often preferable to reasoning about what changed. Less caching, more recomputation, and fewer assumptions about persistence.
That bet pays off in large repositories where incremental correctness is hard to guarantee. If the graph is rebuilt frequently, stale state cannot accumulate in the same way. The failure mode shifts from silent staleness to visible rebuild time, which is easier to reason about and easier to budget for.
The tradeoff is cold starts. A fresh checkout or a cleared cache still costs real time, and teams that rely on warm caches for developer velocity feel that cost daily. Buck2 adopters accept it in exchange for a graph that does not rot in place. There is no free version of this.
Buck2's design also changes how teams think about correctness. When the graph is rebuilt often, the question shifts from "is my cache valid?" to "is my build reproducible?" The latter is a property of the build definition, not of the cache state, and it is easier to test. A team can write a test that runs a clean build and compares outputs; it cannot easily write a test that proves a warm cache is still correct after months of incremental changes.
What This Means for Monorepo Teams
Migration paths between the two systems are rarely clean. Build file syntax overlaps, but the operational assumptions do not. A team moving from Bazel to Buck2 usually rewrites more than it expects, because the value it extracted from Bazel was tied to incremental state that Buck2 does not preserve.
Bazel shops tolerate graph rot because their tooling is built around it. Cache warming jobs, remote execution clusters, and CI pipelines all assume incrementality. Ripping that out means rebuilding the surrounding infrastructure, not just the build files. A related piece on partition ownership tradeoffs makes a similar point about systems whose assumptions harden into infrastructure.
Buck2 adopters accept rebuild costs as a line item. They staff for it, they provision for it, and they stop treating a warm cache as a right. The choice reflects team scars more than benchmarks. Teams that have been burned by stale artifacts tend to prefer recomputation; teams that have been burned by slow CI tend to prefer incrementality.
The decision also affects hiring. A Bazel-heavy team needs engineers who understand remote execution, cache key design, and the subtle interactions between action graphs and file system state. A Buck2-heavy team needs engineers who can profile build performance and optimize the critical path of a clean build. The skill sets overlap but are not identical, and a team that switches systems may find that its existing build engineers need to learn a different set of tools.
The Human Cost of Graph Maintenance
Build engineers become graph janitors. A meaningful share of the role is pruning unused dependencies, untangling cycles, and explaining why a target that nobody owns still blocks a release. The work is necessary and largely invisible in product roadmaps.
Oncall rotations chase stale artifacts. A build that passes on one machine and fails on another consumes hours of investigation, and the root cause is often a dependency that was added two years ago and never removed. Documentation lags behind reality because the graph changes faster than the docs describing it.
Career growth gets tied to cleanup wins. Removing a thousand unused targets or cutting cold build time in half is a measurable achievement, and it is also the kind of work that does not ship features. Build engineers who stay in the role learn to frame maintenance as reliability engineering, which is accurate and still under-credited.
There is also a morale cost. Graph maintenance is often reactive: a release is blocked, an oncall investigates, and the fix is to remove a dependency that should have been removed years ago. The engineer who does that work rarely gets credit for preventing a future incident; they get credit for unblocking a release that someone else needed. That asymmetry makes it hard to justify dedicating headcount to proactive graph hygiene, even when the data supports it.
Practical Steps for Aging Build Graphs
Audit target freshness quarterly. Export the list of targets that have not been built or depended on in the last ninety days and review them with their nominal owners before deleting anything.
Prune unused WORKSPACE dependencies on a schedule. Treat every added dependency as a commitment with a review date, and remove entries whose consumers have disappeared.
Benchmark cold and warm build times separately. A single build-time number hides the cost that matters most, which is the time a new engineer waits on a fresh checkout.
Document graph ownership explicitly. Every top-level target should name a team, and every team should know which targets it owns, because unowned targets are the ones that rot.
Rehearse full rebuilds regularly. Run a clean build in CI on a fixed cadence so that stale cache assumptions surface before they block a release.
Track the ratio of clean builds to incremental builds. A team that never runs clean builds is flying blind on reproducibility, and a team that runs them too often is paying for incrementality it is not using. The right ratio depends on the repository, but measuring it is the first step toward setting it intentionally.