3.1.5 Eager Fetch Anti-pattern

recommended

External repositories in Bazel are supposed to stay mostly lazy: Bazel fetches them when a requested target actually needs something from them. An eager fetch breaks that expectation. A load() from an external repository can force Bazel to materialize that repo while it is still loading the reachable BUILD and .bzl files, so a target that looks unrelated pays dependency setup cost before execution even starts.1,2 The pain shows up on the loading side of 2.2 Three Phases of a Build, not because your target used the dependency directly, but because Bazel needed the external file to understand the package at all.3

Why load() changes the fetch boundary

This uses the same mechanism introduced in 0.3.5 Load Statements. The only twist is that the label points at @repo//... instead of your main repository. External repos are ordinarily fetched on demand, and modern module extensions from 3.1.2 Using Extensions are also lazy by default: Bazel usually waits until a generated repo is brought into scope with use_repo() and actually referenced in a build.1,4,5 Eager fetch is what happens when a load() edge makes that boundary broader than the targets you meant to build.

The important mental model is that Bazel is still trying to construct the graph at this point. If reading one package requires an external .bzl file, Bazel has to make that repository available first. The underlying fetch or setup work lives down in 4.9 Repository Rules. The user-visible symptom is just that a small build suddenly downloads or prepares a large external dependency before any requested action runs.2

The legacy worst case: WORKSPACE eager fetches

The broadest form lives in 3.1.4 Legacy WORKSPACE Model. In the WORKSPACE model, Bazel had to evaluate the whole file to discover what third-party repositories existed, so an external load() there affected every build.2,6

load("@rules_python//python:pip.bzl", "pip_parse")

pip_parse(
    name = "my_deps",
    requirements_lock = "//path/to:requirements_lock.txt",
)

load("@my_deps//:requirements.bzl", "install_deps")
install_deps()

The second load() is the problem. Bazel cannot read @my_deps//:requirements.bzl without first materializing @my_deps, so every build pays the repository rule's setup cost, even if the developer is building something that never touches Python.2

One practical fix is to check in the generated helper file instead of loading it back out of the external repository. That moves the expensive work out of the normal loading path and into an explicit update flow that maintainers can review and test.2

BUILD-file eager fetches are narrower, but sneakier

BUILD-file eager fetches are package-scoped rather than workspace-wide, but that still hurts in a large repo. If a package header does:

load("@pip//:requirements.bzl", "requirement")

then any target that makes Bazel load that package pays for @pip, even a filegroup, test, or helper target in the same directory that has no Python dependencies of its own.2,3 For Python, L3.3 pip Dependency Integration introduces requirement() as the convenient interface for pip.parse. The convenience is real, but it is also exactly where eager fetches hide.2

The same pattern shows up with npm helper repositories and other generated .bzl entry points.2 The rule is simple: if unrelated targets share a BUILD file with an expensive external load(), they inherit that load-time cost.

That is why package boundaries matter. Splitting targets into different packages avoids fetching unnecessary dependencies.6 Keep the targets that truly need the external helper in one package, and move unrelated lightweight targets out so Bazel can load them without touching the heavy repo.

Query the graph you are actually loading

Use bazel query first, not guesswork. query operates on the loading-phase target graph, which is exactly the layer eager fetch distorts.3,7 When you are chasing a load()-driven fetch, start with buildfiles() rather than plain deps(): deps() includes only rule and file targets, while buildfiles() also includes the BUILD and .bzl files needed to load those packages.8

bazel query 'let targets = set(//some:target //some/other:target) in buildfiles(deps($targets))' \
  | uniq | sort | grep @slow_repo

Because buildfiles() walks the loaded BUILD and .bzl files, this catches package-level load() edges that are easy to miss when you only stare at rule attributes.2,8

If you just want a quick audit of ordinary external rule dependencies, this is still useful:

bazel query "filter('@', deps(//path/to:target))"

Just do not mistake it for a complete eager-fetch detector: a repository reached only through a load() edge can still be absent from plain deps() output.8,7 The eager-fetch-load-edge snippet keeps that blind spot small: the requested target has no external dependency, but loading its package still reaches an external .bzl file. For the broader query toolbox, refresh 2.1.5 Inspecting the Graph — Query Preview.

Fix the edge, not just the download

The durable mitigations all change graph shape rather than just hiding latency:

  • Check in generated helper files when a repository rule produces a .bzl file whose only job is to be loaded back by WORKSPACE or BUILD code.2
  • Split mixed BUILD files so targets that need an external helper live in a different package from targets that do not.2,6
  • Avoid convenience helpers in hot paths when they are just syntax sugar over generated labels and they pull a large repository into the loading path.2
  • If the eager fetch is unavoidable, make the fetched repository smaller or cheaper to prepare so the unavoidable cold-start cost is lower.2

--repository_cache and vendor mode are still useful, but they solve a different problem. The repository cache only reuses downloads that Bazel's own downloader can verify by hash. It does not cache arbitrary work delegated to tools like pip or npm.2,3 Vendor mode from 3.1.9 Offline / Air-Gapped Builds can remove network dependence for selected repositories or targets, but it does not change the structural fact that Bazel still has to touch that repo during loading.9

Do not confuse "this fetch is cached" with "this dependency edge is harmless." Caches and vendoring make cold starts less painful. They do not restore the narrower slice of the graph you wanted in the first place. If an unrelated target still has to load @pip or @npm, you still have an eager-fetch design problem.2,9

key takeaway

Treat external load()s as widening the graph slice Bazel must materialize before execution. The maintainer process is simple: prove the unwanted edge with bazel query, remove or relocate helper loads so the edge disappears, and keep expensive external loads isolated to the packages that truly need them.2,8

Check your understanding · 3 questions

1.A BUILD file contains: load('@pip//:requirements.bzl', 'requirement'). Which targets pay the eager-fetch cost of materializing @pip?

Select one answer

2.Which of the following are durable mitigations for eager fetches? Select all that apply.

Select all that apply

3.When using bazel query to detect eager-fetch edges, why should you use buildfiles() rather than plain deps()?

Select one answer

0 of 3 answered

Footnotes

  1. External dependencies overview — external repos are fetched on demand when labels need them 1 2

  2. Bazel: Avoiding eager fetches — WORKSPACE and BUILD-file eager fetches, requirement() pitfalls, query-based detection, and mitigation strategies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

  3. Build programs with Bazel — loading-phase behavior and repository-cache scope 1 2 3 4

  4. Module extensions — extensions are evaluated lazily until generated repos are brought into scope and used

  5. Frequently asked questions — why use_repo() exists and how lazy extension evaluation works

  6. Bzlmod Migration Guide — split packages to avoid fetching unnecessary dependencies 1 2 3

  7. A guide to Bazel query — practical filter() usage and query-family overview 1 2

  8. The Bazel Query Referencedeps() excludes load-only BUILD/.bzl files, while buildfiles() exposes them 1 2 3 4

  9. Vendor Mode — vendoring external repos for offline or controlled builds without changing the logical dependency edge 1 2