1. Resources
  2. /
  3. Blog
  4. /
  5. A guide to Bazel query

A guide to Bazel query

11 minute read

An image of a the Bazel logo and a terminal window showing a Bazel query command.

First, congratulations: If you're reading this, you've likely succeeded in getting a Bazel project set up, configured, and actually built, which—as anyone who's used Bazel before will tell you—is no easy feat. If you haven't already, take a moment to kick back with the beverage of your choice, because that alone is something to celebrate.

With your project up and running, though, you'll soon be faced with another challenge: getting your head around the Bazel dependency graph. Why does this tiny library seem to be pulling in half the internet? Why is this test taking a half-hour to run, when it should take only a few seconds? How are all of these Bazel packages related? And if I change something here, what else might break?

All of these are questions that bazel query, one of several subcommands of the Bazel CLI, can help you answer—but of course, you have to learn how to use it first. So in this post, we'll help you do that with a practical example that shows how to use bazel query to:

  • Explore and visualize your Bazel dependency graph
  • Analyze and trace the relationships between packages in a Bazel workspace
  • Identify and troubleshoot direct, transitive, and third-party dependencies
  • Make informed refactoring decisions safely
  • Optimize your build performance by pinpointing dependency bottlenecks

With a solid understanding of bazel query in your toolbox, you'll not only be able to maintain control over your Bazel-managed codebase as it grows, you'll be able to build more advanced workflows on top of it that help you get the most out of this incredibly useful tool.

Let's dive in.

Bazel query by example: a hypothetical Go monorepo

Throughout this guide, we'll use the shell of a hypothetical Go microservices project as an example to work from. The project's monorepo (which is available on GitHub if you'd like to follow along and run these commands yourself—and you should!) contains a single Bazel workspace with a handful of library and service packages:

1
2
3
4
5
6
//services/auth:auth         # Authentication microservice
//services/user:user         # User management microservice
//services/payment:payment   # Payment processing microservice
//lib/logging:logging        # Shared logging utilities
//lib/config:config          # Configuration management
//lib/metrics:metrics        # Metrics collection

Each service is a go_binary target, and the libraries are all go_library targets shared across those services. Here, for example, is what the BUILD.bazel file looks like for the payment service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ./services/payment/BUILD.bazel
load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test")

go_library(
    name = "payment_lib",
    srcs = [
        "handlers.go",
        "postgres.go",
        "service.go",
    ],
    importpath = "bazel_query_example/services/payment",
    visibility = ["//visibility:private"],
    deps = [
        "//lib/config",
        "//lib/logging",
        "//lib/metrics",
        "//services/payment/db",
        "@com_github_lib_pq//:pq",
    ],
)

go_binary(
    name = "payment",
    embed = [":payment_lib"],
    visibility = ["//visibility:public"],
)

Now let's start exploring this codebase with bazel query.

The basic query: Understanding your workspace

The most fundamental query lists all of the targets in your Bazel workspace:

1
bazel query //...

With our example, this might show a dozen or so targets, but in a larger project, it could surface hundreds or even thousands, which might not be useful. You can get more specific by narrowing for the kinds of targets you're looking for—for example, for a list of the Go binaries defined anywhere in the services package of your workspace:

1
bazel query "kind('go_binary', //services/...)"

In our case, that'd return:

1
2
3
//services/auth:auth
//services/payment:payment
//services/user:user

The kind() function filters targets by their rule type. In Bazel terminology, a rule is essentially a function that defines how to build a particular output (like a binary or a library), given some set of inputs. The go_binary rule compiles Go code into an executable binary.

Exploring dependencies with deps()

To understand what a specific service depends on:

1
bazel query "deps(//services/payment:payment)"

This reveals all dependencies of the payment service, including both direct and transitive dependencies. The output might include targets like:

1
2
3
4
5
6
7
8
//lib/logging:logging
//lib/config:config
//lib/metrics:metrics
...
@bazel_tools//tools/allowlists/function_transition_allowlist:function_transition_allowlist
@bazel_tools//tools/cpp:optional_current_cc_toolchain
@bazel_tools//tools/cpp:toolchain_type
...

Notice how this includes not only our internal libraries, but also external dependencies (those prefixed with @). The deps() function traverses the entire dependency graph, showing every target required to build a given service.

To focus on direct dependencies only, you can add a second depth parameter:

1
bazel query "deps(//services/payment:payment, 1)"

Which might instead return only these:

1
2
//services/payment:payment
//services/payment:payment_lib

Notice that payment_lib is listed because the go_binary rule (from above) uses the embed attribute rather than deps. The binary embeds the library, and the library depends on everything else. To see one more level:

1
bazel query "deps(//services/payment:payment, 2)"

Now you'd see something like:

1
2
3
4
5
6
7
8
9
10
//lib/config:config
...
//services/payment:payment
//services/payment:payment_lib
...
@bazel_tools//tools/allowlists/function_transition_allowlist:function_transition_allowlist
@com_github_lib_pq//:pq
@rules_go//:cgo_context_data
@rules_go//:go_config
...

This layered approach to querying helps you understand the structure of your build targets without being overwhelmed by information.

Identifying dependents with rdeps()

Perhaps even more powerful than understanding what a given package depends on is understanding what other packages depends on it. Imagine, for example, you wanted to make a change to the logging library. Which other packages in the Bazel workspace would will be affected?

1
bazel query "rdeps(//..., //lib/logging:logging)"

This would show:

1
2
3
4
5
6
7
//lib/logging:logging
//services/auth:auth
//services/auth:auth_lib
//services/payment:payment
//services/payment:payment_lib
//services/user:user
//services/user:user_lib

The rdeps() function (short for reverse dependencies) works in the opposite direction of deps(). Rather than asking, "What does this package depend on?", it asks instead, "What else depends on this package?" — an important question to be able to answer in assessing the impact of changes to shared components in a large monorepo.

If you wanted to narrow it down to just the binaries (excluding the libraries):

1
bazel query "kind('go_binary', rdeps(//..., //lib/logging:logging))"

You'd get:

1
2
3
//services/auth:auth
//services/user:user
//services/payment:payment

This combination of functions creates some powerful filtering capabilities.

Visualizing dependency paths with somepath() and allpaths()

Determining exactly how one target depends on another can be helpful for debugging unexpected dependencies as well. Say you're puzzled by the payment service's dependency on some third-party database driver. You could quickly get to the bottom of that mystery with the somepath() function:

1
bazel query "somepath(//services/payment:payment, @com_github_lib_pq//:pq)"                

Which would reveal that the path to the dependency (pq)— is through payment_lib:

1
2
3
//services/payment:payment
//services/payment:payment_lib
@com_github_lib_pq//:pq

The somepath() function lets you see the exact path of dependencies, making it clear exactly what's responsible for the database driver getting pulled in.

But what if the path is more complex? Imagine you refactored the payment service to use a database interface instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# In services/payment/BUILD.bazel...
#...

# Add a go_library definition for the interface.
go_library(
    name = "postgres_adapter",
    srcs = ["postgres.go"],
    deps = [
        "//services/payment/db:db",
        "@com_github_lib_pq//:pq",
    ],
)

go_library(
    name = "payment_lib",
    srcs = [
        "handlers.go",
        "postgres.go",
        "service.go",
    ],
    importpath = "bazel_query_example/services/payment",
    visibility = ["//visibility:private"],
    deps = [
        "//lib/config",
        "//lib/logging",
        "//lib/metrics",
        "//services/payment/db",
        
        # Add this line to the `payment_lib` to use this new interface.
        ":postgres_adapter", 
    ],
)

# ...

Now that same query would show both the addition of the new postgres_adapter interface and that the new interface is what's responsible for the transitive dependency of payment on pq:

1
2
3
4
//services/payment:payment
//services/payment:payment_lib
//services/payment:postgres_adapter
@com_github_lib_pq//:pq

The somepath() function identifies a single (randomly selected) dependency path. For a more comprehensive view of all possible dependency paths, you can use the aptly named allpaths() function:

1
bazel query "allpaths(//services/payment:payment, @com_github_lib_pq//:pq)"

This would identify all of the paths to pq, from anywhere in the //services/payment:payment package.

To generate a visual representation of these dependencies, you can use --output graph:

1
bazel query 'deps(//services/payment/..., 2) ' --output graph --noimplicit_deps  | dot -Tpng -o graph.png

Assuming you have Graphviz installed, this would generate a Graphviz diagram in PNG format that visualizes the entire dependency tree—a great resource for coming to a quick understanding of complex projects when text-based representations become unwieldy. Here, we're showing the dependencies of the payment service to a depth of 2, as described above:

A GraphViz diagram showing the dependencies of the //services/payment package.

A GraphViz diagram showing the dependencies of the //services/payment package.

Filtering by target type with kind()

In a large monorepo, you'll often have a mix of libraries, binaries, and tests. To focus specifically on only the tests, you can combine kind with the go_test type:

1
bazel query "kind('go_test', //...)"

This can be especially helpful when you need to run specific commands using certain types of targets—for example, running all of the tests for a given package:

1
bazel test $(bazel query "kind('go_test', //services/payment/...)")

Which for our project would yield:

1
2
3
4
5
6
7
8
9
10
INFO: Analyzed target //services/payment:payment_test (138 packages loaded, 5585 targets configured).
INFO: Found 1 test target...
Target //services/payment:payment_test up-to-date:
  bazel-bin/services/payment/payment_test_/payment_test
INFO: Elapsed time: 31.644s, Critical Path: 15.89s
INFO: 64 processes: 6 internal, 58 darwin-sandbox.
INFO: Build completed successfully, 64 total actions
//services/payment:payment_test                              PASSED in 0.3s

Executed 1 out of 1 test: 1 test passes.

This pattern—of using query results as input to bazel or other shell commands—is a powerful way to compose dynamic workflows in your software-delivery pipelines.

Combining queries for powerful insights

The real power comes from combining these functions to form more complex queries. For instance, to find all tests that depend on the metrics library (perhaps because you're considering updating the metrics interface):

1
bazel query "kind('go_test', rdeps(//..., //lib/metrics:metrics))"

This might reveal:

1
2
3
4
//lib/metrics:metrics_test
//services/auth:auth_test
//services/payment:payment_test
//services/user:user_test

Now you know exactly which tests might be affected by your changes.

Or to identify any services that depend on both the logging and config libraries:

1
bazel query "kind('go_binary', rdeps(//..., //lib/logging:logging)) intersect kind('go_binary', rdeps(//..., //lib/config:config))"

The result:

1
2
3
//services/auth:auth
//services/payment:payment
//services/user:user

In our case, all three of our services use both libraries, but in a larger codebase, queries for intersections like these could identify unexpected dependency relationships or architectural choices that may call for a closer look and/or some refactoring.

Beyond the basics with cquery and aquery

As your Bazel workspace grows, you may find yourself wanting to understand how specific build flags or select() statements impact your build graph. This is where Bazel's cquery —or "configurable query"—command comes in.

As an example, if our hypothetical payment service were using select() with the deps attribute to toggle between different dependencies by deployment environment—e.g., with the following modified version of the payment service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Again, in services/payment/BUILD.bazel...
# ...

go_library(
    name = "payment_lib",
    srcs = ["service.go", "handlers.go"],
    deps = [
        "//lib/config:config",
        "//lib/logging:logging",
        "//lib/metrics:metrics",
        "//services/payment/db:db",
    
    # To make dependencies selectable/configurable: 
    ] + select({
        "//config:production": ["//lib/tracing:datadog"],
        "//config:development": ["//lib/tracing:jaeger"],
        "//conditions:default": [],
    }),
)

# ...

... you could combine bazel cquery with the somepath() function to confirm that in production, the service uses the datadog library for tracing:

1
bazel cquery "somepath(//services/payment:payment, //lib/tracing:datadog)" --define environment=production
1
2
3
4
INFO: Found 2 targets...
//services/payment:payment (91a5247)
//services/payment:payment_lib (91a5247)
//lib/tracing:datadog (91a5247)

The standard bazel query command wouldn't be able to show this, as query operates on Bazel's loading phase (more on this below). But cquery, which operates on the analysis phase, would, as it's there that select() statements are evaluated, based on the build flags you provide to the Bazel CLI.

For even deeper insights into how Bazel actually builds your targets, the aquery (or "action query") command reveals the precise commands (i.e., actions) that are executed:

1
bazel aquery "//services/payment:payment"

Which would render, for example:

1
2
3
4
5
6
7
8
9
10
11
action 'GoCompilePkg services/payment/payment.a'
  Mnemonic: GoCompilePkg
  Target: //services/payment:payment
  Configuration: darwin_arm64-fastbuild
  Execution platform: @@platforms//host:host
  ActionKey: cdc5684315545c6d69266f14ad9568cb87c401b0ef775ce97a9fb57a1a5dd979
  Inputs: [bazel-out/darwin_arm64-fastbuild/bin/external/gazelle++go_deps+com_github_lib_pq/pq.x, bazel-out/darwin_arm64-fastbuild/bin/external/rules_go+/stdlib_/pkg, bazel-out/darwin_arm64-fastbuild/bin/lib/config/config.x, bazel-out/darwin_arm64-fastbuild/bin/lib/logging/logging.x, bazel-out/darwin_arm64-fastbuild/bin/lib/metrics/metrics.x, bazel-out/darwin_arm64-fastbuild/bin/services/payment/db/db.x, bazel-out/darwin_arm64-opt-exec-ST-d57f47055a04/bin/external/rules_go++go_sdk+go_default_sdk/builder_reset/builder, bazel-out/darwin_arm64-opt-exec-ST-d57f47055a04/bin/external/rules_go++go_sdk+go_default_sdk/packages.txt, external/rules_go++go_sdk+go_default_sdk/bin/gofmt, external/rules_go++go_sdk+go_default_sdk/go.env, external/rules_go++go_sdk+go_default_sdk/pkg/include/asm_amd64.h, external/rules_go++go_sdk+go_default_sdk/pkg/include/asm_ppc64x.h, external/rules_go++go_sdk+go_default_sdk/pkg/include/funcdata.h, external/rules_go++go_sdk+go_default_sdk/pkg/include/textflag.h, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/addr2line, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/asm, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/buildid, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/cgo, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/compile, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/covdata, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/cover, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/doc, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/fix, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/link, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/nm, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/objdump, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/pack, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/pprof, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/test2json, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/trace, external/rules_go++go_sdk+go_default_sdk/pkg/tool/darwin_arm64/vet, services/payment/handlers.go, services/payment/postgres.go, services/payment/service.go]
  Outputs: [bazel-out/darwin_arm64-fastbuild/bin/services/payment/payment.a, bazel-out/darwin_arm64-fastbuild/bin/services/payment/payment.x]
  Environment: [CGO_ENABLED=1, GOARCH=arm64, GODEBUG=winsymlink=0, GOEXPERIMENT=nocoverageredesign, GOOS=darwin, GOPATH=, GOROOT_FINAL=GOROOT, GOTOOLCHAIN=local, PATH=external/rules_cc++cc_configure_extension+local_config_cc:/usr/bin:/bin, ZERO_AR_DATE=1]
  Command Line: (exec bazel-out/darwin_arm64-opt-exec-ST-d57f47055a04/bin/external/rules_go++go_sdk+go_default_sdk/builder_reset/builder \
  ...

The output here can be a bit overwhelming, as it includes every compilation action, linking step, and so on—showing the actual compiler flags, environment variables, and command lines used during the build. But it's definitely valuable data to be able to reach for to debug complex build issues or understanding the optimizations Bazel is applying.

Practical scenarios in development and delivery

Let's wrap up with a few more practical situations in which Bazel query can be useful.

Optimizing build performance

If your CI builds are slow, you can identify the most distant dependencies with minrank:

1
bazel query "deps(//services/...)" --output minrank | sort -rn | head -10

The minrank operator sorts targets by their depth in the graph, or how far they are from their original targets, which can help you focus your optimization efforts on the most impactful targets.

Safely removing dead code

Before deleting what appears to be unused code, you can query for any dependencies on it:

1
bazel query "rdeps(//..., //lib/legacy:some_old_util)"

If bazel query reports nothing, it's likely safe to remove.

Auditing third-party dependencies

To list all external dependencies your Bazel packages have pulled in, you can use the filter() function with the @ symbol (which Bazel uses to prefix external/third-party dependencies):

1
bazel query "filter('@', deps(//services/...))"

This command is particularly valuable for security audits, but is useful in many ways, including helping to identify:

  • Outdated dependencies that need updating
  • Duplicate dependencies with differing versions
  • Unexpected dependencies that might introduce vulnerabilities

Understanding Bazel's build phases

To get the most out of the Bazel query, cquery, and aquery, it helps to understand how each one relates to Bazel's build phases:

  1. Loading phase: Here, Bazel loads BUILD files and constructs the target graph. The query command operates at this level, working with the theoretical structure of your build, without considering any build flags.
  2. Analysis phase: Here, Bazel applies the build the options that you provide at the command line and uses them to evaluate the select() statements in your BUILD files. The cquery command operates at this level, showing you what the build graph actually looks like after configuration.
  3. Execution phase: Here, Bazel determines the precise actions needed to build each target. The aquery command operates at this level, showing the actual commands that are executed during the build.

Each query type offers a different perspective on your build, allowing you to examine exactly what you need at different stages of the build process.

Wrapping up, and next steps

We've covered a lot! But by now it should be clear that bazel query is more than just a handy tool—it's an interactive lens that can bring clarity and maintainability to even the most complex Bazel workspaces. Combined with other tools, it can unlock even more advanced workflows that allow you to tailor your delivery processes and get the most out of what Bazel has to offer.

Some suggestions to keep the learning going:


Related posts

Start turning complexity into an advantage

Create an account to get started with a 30-day free trial. No credit card required.

Buildkite Pipelines

Platform

  1. Pipelines
  2. Pipeline templates
  3. Public pipelines
  4. Test Engine
  5. Package Registries
  6. Mobile Delivery Cloud
  7. Pricing

Hosting options

  1. Self-hosted agents
  2. Mac hosted agents
  3. Linux hosted agents

Resources

  1. Docs
  2. Blog
  3. Changelog
  4. Webinars
  5. Plugins
  6. Case studies
  7. Events

Company

  1. About
  2. Careers
  3. Press
  4. Brand assets
  5. Contact

Solutions

  1. Replace Jenkins
  2. Workflows for AI/ML
  3. Testing at scale
  4. Monorepo mojo
  5. Bazel orchestration

Support

  1. System status
  2. Forum
© Buildkite Pty Ltd 2025