Bazel 9.1.0: What Changed and How to Think About the Upgrade

Posted on in Programming

Bazel 9.1.0 is not the kind of release that should make an engineering team drop everything and schedule a build-system migration party. It is a minor LTS release in the Bazel 9 line, published on April 20, 2026, and most of the changes are incremental. That is good news. The best build-system releases are usually the ones that let you keep shipping software while quietly removing some friction from the machinery.

That said, "minor" does not mean "ignore it." Bazel 9 is already a meaningful line in the sand: WORKSPACE support is gone, built-in language rules have moved out into external modules, and teams are expected to be living in the Bzlmod world now. Bazel 9.1.0 sits on top of that foundation. It mostly tightens screws, adds useful remote execution and repository-cache behavior, and exposes a couple of compatibility issues that are worth understanding before you bump the version in CI.

If you have not already read the earlier Slaptijack article on Bazel 8.0.0 and what it means for build pipelines, start there. Bazel 8 was the transition release. Bazel 9 is where many of those warnings became reality.

Bazel 9.1.0 in Context

The short version: Bazel 9.1.0 is a minor release on the Bazel 9 LTS track. The official release notes describe it as backward compatible with Bazel 9.0, with two exceptions: the CcInfo change and --downloader_config behavior. The release model matters here because Bazel 9 is the active LTS line, while Bazel 8 has moved into maintenance and Bazel 6 is deprecated.

That should influence how you prioritize the work. If you are already on Bazel 9.0.x, 9.1.0 is a reasonable upgrade candidate after normal CI validation. If you are still on Bazel 7 or 8, this is not just a patch-level jump. You are crossing the Bzlmod and Starlarkification boundary, and that deserves a proper upgrade branch, a representative CI matrix, and time to clean up ruleset versions.

Bazel 9.0 completed several major changes:

  • Bzlmod fully replaced the legacy WORKSPACE system.
  • Previously built-in language rules moved out into external Starlark modules.
  • Rules now need to be loaded explicitly instead of relying on the old built-in surface.
  • Protobuf gained a path toward using prebuilt protoc rather than rebuilding the compiler as often.

Bazel 9.1.0 is best understood as the first minor release that makes those new assumptions feel more normal.

The Big Compatibility Note: CcInfo

The most important practical note in Bazel 9.1.0 is the CcInfo error:

The CcInfo symbol has been removed

This is tied to C++ Starlarkification. The confusing part is that the underlying change was already present in Bazel 9.0.0, but a bug fixed in 9.0.1 caused the error to surface more clearly. In other words, 9.1.0 may look like it broke your build, but it is more accurate to say that it is now telling you about a problem that was already there.

For many teams, the fix will not be in your application code. It will be in your rulesets. The official release notes specifically call out upgrading broken rulesets such as rules_go and rules_nodejs, with example versions:

bazel_dep(name = "rules_go", version = "0.59.0")
bazel_dep(name = "rules_nodejs", version = "6.7.3")

That does not mean those are the only versions you should ever use. It means you should treat ruleset versions as first-class parts of the upgrade. If your Bazel bump happens in .bazelversion but your MODULE.bazel is full of stale rule dependencies, you are only doing half the work.

My practical recommendation:

  • Upgrade Bazel and core rulesets together in a single branch.
  • Run representative builds for every major language stack in the repo.
  • Look for direct references to old C++ providers or ruleset versions that predate Bazel 9 support.
  • Avoid papering over the problem with local compatibility hacks unless you are buying a short, explicit migration window.

Build systems get fragile when the tool version and the ruleset ecosystem drift apart. Bazel 9 makes that more obvious.

--downloader_config: Useful, But Temporarily Awkward

Bazel 9.1.0 allows --downloader_config to be specified multiple times, so a build can use several downloader configuration files at once. That is genuinely useful for organizations with layered configuration. For example, a company might have one baseline downloader policy for mirrors and authentication, then a repository-specific layer for special cases.

The wrinkle is that the Bazel release notes call this a technical breaking change and say it will be reverted in future Bazel 9.x releases starting with 9.1.1. The same incompatible behavior is expected to return in Bazel 10.x.

That is a little odd, but the operational advice is simple: do not build a long-lived Bazel 9 workflow that depends on multiple --downloader_config flags unless you are deliberately pinning 9.1.0 and accepting that constraint.

For most teams, I would treat this as a preview of where Bazel is going rather than a feature to standardize on immediately. If you need multiple downloader config files today, test carefully and document the Bazel version assumption in your CI configuration. Otherwise, keep your downloader configuration boring until the Bazel 10 behavior lands.

Better Test Output for Cached Results

One small but welcome CLI improvement is the addition of two test summary modes:

  • --test_summary=short_uncached
  • --test_summary=detailed_uncached

These suppress reporting of cached test results. That sounds cosmetic until you have a large CI job with thousands of tests, most of which are cache hits. In that environment, noisy test summaries are not harmless. They make it harder to find the tests that actually ran, the tests that failed, and the signal that a human needs to inspect.

This is one of those quality-of-life flags that is worth trying in CI output. You still want enough test visibility for debugging, but you probably do not need every cached result shouting at you every run.

I would start with:

bazel test //... --test_summary=short_uncached

Then use the detailed form in jobs where developers regularly need richer test metadata for uncached execution. The goal is not to hide information. The goal is to make the information density better.

External Dependencies and Repository Cache Improvements

Bazel 9.1.0 includes several changes around external dependencies and repository content caching. These are not flashy, but they matter for teams trying to make remote and local builds more predictable.

First, package_group now supports labels with external repositories in the packages attribute. That gives teams more expressive visibility modeling when external repositories are part of the boundary. If you run a monorepo with internal modules, generated repos, and shared rulesets, these little visibility improvements can remove surprising workarounds.

Second, rctx.symlink now implicitly watches the target if it falls back to a copy. That is the kind of repository-rule correctness detail most developers do not want to think about, but ruleset authors absolutely should. Repository rules are only pleasant when they invalidate at the right time.

Third, Bazel now includes the host operating system and CPU architecture in the local and remote repo contents cache key. That should reduce a class of cross-platform cache confusion where a repository output is not as portable as it first appears. If you build on macOS laptops, Linux CI workers, and maybe a mix of x86_64 and ARM machines, this is the sort of change that helps the cache behave more honestly.

Finally, the remote repo contents cache now supports all reproducible repository rules. That is a step toward making dependency fetching less wasteful and more consistent across machines.

The broader theme is hermeticity. If you care about why these details matter, see The Benefits of Hermeticity in Modern Code Repositories and Hermeticity in Software Development: A Comprehensive Guide. Bazel is still pushing more state into explicit, cacheable, reproducible surfaces. That is the right direction.

Remote Execution: Recovering From Lost Inputs

Bazel 9.1.0 adds experimental support for --rewind_lost_inputs, which can rerun actions within a single build to recover from lost inputs caused by remote or disk cache evictions.

If you have never operated a large remote execution or remote cache setup, that may sound like an edge case. It is not. Caches are systems. Systems have evictions, races, partial failures, policy changes, and maintenance windows. When a build fails because an input that was supposed to exist has disappeared, the technically correct answer may be "your cache had a bad moment," but that is not satisfying to the developer who just wants the build to finish.

The idea behind rewinding lost inputs is pragmatic: if an action's inputs are no longer available, Bazel can rerun enough work inside the same build to recover. Because the flag is experimental, I would not turn it on everywhere without measurement. But I would absolutely test it in CI environments where cache eviction or remote execution flakiness is a known source of pain.

Bazel 9.1.0 also adds --experimental_remote_cache_chunking, which can read and write large blobs to and from the remote cache in chunks. This requires server support, so it is not a magic client-only improvement. Still, it is worth noting if you operate your own cache infrastructure or work with a remote execution vendor. Large artifact handling is one of the places where build performance often turns into infrastructure performance.

Starlark String Behavior: A Small Correctness Fix

The Starlark change in 9.1.0 is wonderfully specific: string.splitlines() no longer incorrectly treats Unicode U+0085, also known as NEL, as a newline character.

Most teams will never notice this. That is fine. But for ruleset authors, generators, code analysis tools, or build macros that process text in Starlark, it is a reminder that build languages have language semantics too. Tiny string behavior changes can become real if they affect generated BUILD files, metadata parsing, or platform-specific tooling.

This is not a reason to fear the upgrade. It is a reason to have tests for your rules and macros, especially if your repository relies on custom Starlark logic.

How I Would Approach the Upgrade

If I were responsible for a serious Bazel-based repo, I would not just change .bazelversion and hope. I would make the upgrade boring on purpose.

Start by upgrading on a branch:

9.1.0

Then inspect your MODULE.bazel:

rg 'bazel_dep|rules_go|rules_nodejs|rules_cc|rules_java|rules_python|protobuf' MODULE.bazel

Make sure the language rulesets you actually depend on have Bazel 9-compatible versions. Pay special attention to Go, Node.js, C++, Java, Python, and protobuf because those are exactly the areas where the Bazel 8-to-9 transition changed the shape of the ecosystem.

Next, run a representative CI subset before you run everything:

bazel test //...

If your repository is too large for that to be useful as a first pass, run the targets that exercise each ruleset family. A green Java-only build does not tell you whether your Go, protobuf, or frontend rules are ready.

Finally, pay attention to the failure modes:

  • CcInfo errors usually mean stale or incompatible rulesets.
  • External dependency errors may indicate unfinished Bzlmod migration work.
  • Downloader behavior should be reviewed if you use --downloader_config.
  • Remote execution issues should be tested with your actual remote cache and execution infrastructure, not just on a laptop.

That may sound fussy, but it is cheaper than debugging a build-system outage after the version bump lands on main.

Conclusion

Bazel 9.1.0 is a practical maintenance release for teams already moving through the Bazel 9 world. It is not as dramatic as Bazel 8.0 or Bazel 9.0, but it is useful. The release improves CI output for cached tests, strengthens external dependency behavior, adds experimental recovery options for remote execution, and continues the cleanup around Starlarkification and Bzlmod.

The main thing to remember is that Bazel upgrades are ecosystem upgrades. The core binary matters, but so do rules_go, rules_nodejs, rules_cc, rules_java, rules_python, protobuf, your remote cache, your downloader configuration, and your custom Starlark code.

Upgrade deliberately. Keep the rulesets close. Let CI tell you the truth before developers do.

Sources: Bazel 9.1.0 release notes, Bazel 9 LTS announcement, and the Bazel release model.

For more practical build-system and developer productivity notes, visit slaptijack.com.

Slaptijack's Koding Kraken