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
WORKSPACEsystem. - 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
protocrather 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:
CcInfoerrors 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.