Pinning External Dependencies with Bazel Bzlmod: Replace WORKSPACE with MODULE.bazel
Learn how to use Bazel's Bzlmod system to declare dependencies in MODULE.bazel, generate a reproducible lock file, and avoid WORKSPACE drift.
04 Jun 2026, 13:12 UTC

Why use Bzlmod for dependency pinning
Bazel’s Bzlmod system replaces the legacy WORKSPACE file with a declarative MODULE.bazel manifest. When you enable Bzlmod, Bazel resolves each bazel_dep against configured registries, intersects version constraints, and writes a .bzlmod lock file that records the exact revision chosen for every transitive dependency. This makes builds hermetic across machines and lets you review dependency changes as ordinary code diffs.
Worked example: a minimal MODULE.bazel
# MODULE.bazel at the repository root
module(name = "myapp", version = "0.1.0")
# Direct dependencies with explicit versions
bazel_dep(name = "rules_python", version = "0.31.0")
bazel_dep(name = "bazel_skylib", version = "1.5.0")
Save this file, then run the resolution command from the repository root:
# Resolve dependencies and generate the lock file
bazel mod deps --enable_bzlmod
This command contacts the Bazel Central Registry (or any custom registry you have configured), selects the newest versions that satisfy the constraints, and creates .bzlmod next to MODULE.bazel. No further flags are needed for subsequent builds.
Building with the locked versions
Once the lock file exists, a normal build uses the pinned versions automatically:
bazel build //... --enable_bzlmod
If you run the same build again, Bazel will not re‑download or re‑resolve; it reads the exact commits from .bzlmod. To verify that the lock is stable, compare the file before and after a second invocation:
cp .bzlmod .bzlmod.first
bazel build //... --enable_bzlmod
diff .bzlmod.first .bzlmod # should show no differences
Limits and common mistakes
- Bazel version sensitivity. Stable Bzlmod semantics arrived in Bazel 6 and have evolved in 7 and 8. Using an older Bazel will ignore
--enable_bzlmodor behave differently. Check your Bazel version withbazel versionand upgrade if needed. - Mixed WORKSPACE and Bzlmod repositories. If some third‑party rules still rely on WORKSPACE
http_archiveorgit_repository, you may need to keep a minimal WORKSPACE for compatibility or use the--ignore_workspaceflag. Consult each rule’s migration status. - Over‑constraining versions. Declaring a tight version range (e.g.,
version = ">=0.30.0,<0.32.0") can cause resolution to fail when a transitive dependency requires a version outside that range. The error will mention the conflicting module; resolve it by adding an explicitoverridein MODULE.bazel or relaxing the constraint. - Forgetting
--enable_bzlmod. Commands likebazel testorbazel runmust include the flag unless you setbuild --enable_bzlmodin your.bazelrc. Omitting it falls back to WORKSPACE resolution, producing unpredictable builds. - Editing the lock file manually. The
.bzlmodfile is generated; manual edits are overwritten on the nextbazel mod depsand can lead to mismatched expectations. Treat it as read‑only.
Practical verification steps
- Create a fresh directory, add the MODULE.bazel example above.
- Run
bazel mod deps --enable_bzlmodand confirm.bzlmodappears. - Inspect the lock file:
cat .bzlmodshould contain entries likerules_python@0.31.0andbazel_skylib@1.5.0with exact commit hashes. - Run a build:
bazel build //... --enable_bzlmodand note success. - Repeat the build and diff the lock file as shown earlier; no change indicates stable resolution.
- To test a version bump, change
rules_pythonto0.32.0, re‑runbazel mod deps --enable_bzlmod, and verify that the lock file updates only for that module (and any affected transitives).
Following these steps gives you a reproducible dependency graph that can be reviewed like any other source file, eliminating the hidden drift that often accumulates in WORKSPACE‑based setups.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.