TL;DR: Bazel's Build Event Protocol (BEP) is a structured event graph for the command, targets, tests, outputs, failures, and aggregate build metrics. It is machine-readable and far safer than parsing console output, but it is not a complete cache, critical-path, remote-execution, or cost record. Those diagnoses require BEP plus other Bazel and infrastructure telemetry.
The Problem: Builds Are Black Boxes
You run bazel build //.... It prints some output. Eventually it says "Build completed successfully" or something breaks and you scroll up looking for the error. Either way, your interaction with the build is essentially pass/fail plus some scrolling.
But a lot happened inside that build. Bazel resolved a dependency graph, scheduled actions, checked caches, ran tests, and aggregated results. BEP exposes an important structured subset of that lifecycle; profiles, execution logs, and remote-execution systems expose complementary performance evidence.
Terminal output is designed for humans skimming for errors. It is not designed for answering questions like: Why was this build 40% slower than yesterday? Which actions are consistently missing the remote cache? What percentage of my test execution time comes from three flaky tests? How much does this build actually cost to run remotely?
Those questions require structured data from several sources. BEP is the invocation backbone that lets those sources be correlated.
What BEP Actually Is
The Build Event Protocol is a structured stream of protobuf-encoded events that Bazel can publish for a command. Configure a BEP file flag or a BES backend to capture that stream.
Each event represents something meaningful that Bazel chose to publish: the command started, a target was configured or completed, a test produced a result, an output set was named, or the command finished. Every event has an ID and a typed payload. Events announce expected child IDs, forming a directed acyclic graph that lets consumers detect whether the stream is complete.
The key distinction is that BEP is not a console log. It has a defined protobuf schema, typed fields, and explicit event relationships. The schema evolves, so consumers must handle optional, added, and deprecated fields.
How It Works (The Simple Version)
As Bazel runs a build, it emits BEP events in real-time. You can capture these events in two ways: writing them to a local file, or streaming them to a remote server.
Option 1: Write to a local file
Add a flag and Bazel will dump the entire event stream to a file on disk. The JSON format is the easiest to start with:
# Add to your bazel command or .bazelrc
build --build_event_json_file=/tmp/bep.json
# Or for the binary protobuf format (smaller, faster to parse)
build --build_event_binary_file=/tmp/bep.pbAfter the build completes, that file contains the BEP events Bazel published. You can open the JSON file in an editor, pipe it through jq, or parse it programmatically.
Option 2: Stream to a remote server
For production use, you typically want events streamed in real-time to a backend service using the Build Event Service (BES) protocol. This is a gRPC service that Bazel connects to at the start of each build:
build --bes_backend=grpcs://bep.your-service.com:443
build --bes_results_url=https://dashboard.your-service.com/invocation/The --bes_backend flag tells Bazel where to send events. The optional --bes_results_url supplies a base URL that Bazel combines with the invocation ID and prints for the user; whether that destination supports a live view depends on the backend and UI.
What You Get from BEP
The event stream is rich. Here are the key data points BEP gives you, and why each one matters:
- Command outcome and timing. Start/finish information, the authoritative exit code, and aggregate wall/CPU timing. Use a Bazel profile for critical-path and detailed phase analysis.
- Target outcomes. Published target and configuration events, completion or aborted outcomes, and referenced output groups.
- Published action details. Failed actions are included by default. Enable
--build_event_publish_all_actionsto request successfulActionExecutedevents too; available fields and strategy details vary. - Test results. Per-test, per-shard, per-run outcomes. Pass, fail, flaky, timeout — with execution duration and log file references for each.
- Aggregate action metrics.
BuildMetricscan include action and action-cache statistics, but it does not explain which key-forming input caused a miss. - Command line and options. The exact command that was invoked, including all resolved flags from
.bazelrcfiles, command-line arguments, and defaults. Invaluable for debugging why two builds behaved differently. - Configuration identity. Configuration IDs plus available fields such as mnemonic, platform name, CPU, and make variables. Standard BEP does not enumerate the selected toolchains.
- Build metrics summary. Action, target, package, timing, and optional memory metrics emitted at the end of the command. These summarize work Bazel reports for that invocation.
To make this concrete, here is a deliberately partial JSON representation of an action event. Optional fields depend on the action, flags, and Bazel version:
{
"id": {
"actionCompleted": {
"primaryOutput": "bazel-out/k8-fastbuild/bin/src/libutil.a",
"label": "//src:util"
}
},
"action": {
"type": "CppCompile",
"exitCode": 0,
"startTime": "2026-02-15T10:22:01.334Z",
"endTime": "2026-02-15T10:22:01.337Z"
}
}The event identifies the action and can report its outcome, timing, command line, output, and optional strategy details. It does not, by itself, provide a portable cache-miss reason or the complete input comparison needed to explain an action-key change.
BEP vs. Build Logs
If you have ever tried to extract build metrics by parsing Bazel's console output, you already know why BEP exists. But it is worth being explicit about the differences, because many teams start with log parsing before discovering BEP.
Bazel's terminal output is designed for humans. It uses colors and progress bars. It truncates long lines. It dynamically rewrites output. It interleaves progress indicators with errors. It changes format between Bazel versions. It is useful for watching a build in real-time, and it is terrible for programmatic analysis.
BEP is designed for machines. It has a protobuf schema, typed fields, and explicit event relationships. You can consume supported metrics and outcomes without relying on console-output regexes.
The practical consequence is resilience. Console parsing can break when formatting or verbosity changes; a BEP consumer can handle schema evolution explicitly and rely on defined fields rather than output regexes.
Who Uses BEP
BEP is not a niche feature. It is the standard mechanism for getting structured data out of Bazel, and the entire ecosystem of build tooling builds on top of it.
Build observability platforms can use BEP as an invocation data source. When you point --bes_backend at a service, Bazel streams BEP through the Build Event Service protocol. Richer cache, performance, and infrastructure insights require the platform to correlate BEP with other telemetry.
CI systems use BEP to extract test results, build durations, and failure information for reporting and notification. Instead of parsing terminal output for "FAILED" lines, a CI integration reads the structured TestResult and TargetComplete events.
Internal tooling teams build custom dashboards on BEP and complementary data—tracking outcomes and test history, then joining profiles, execution logs, and infrastructure metrics for deeper performance and cost analysis.
If you are doing anything with Bazel beyond running builds manually and looking at terminal output, BEP is almost certainly the right data source.
Getting Started in 5 Minutes
You can start capturing BEP data right now with zero infrastructure. Add two lines to your .bazelrc file at the root of your workspace:
# .bazelrc — add these lines
build --build_event_json_file=build_events.json
build --build_event_publish_all_actionsThe first flag tells Bazel to write the BEP stream as JSON to a local file after every build. The second flag ensures that events are emitted for all actions, not just failed ones (by default, Bazel only emits ActionExecuted events for actions that fail).
Now run a build:
bazel build //...
# After the build, inspect the events:
# Count events by type
cat build_events.json | python3 -c "
import sys, json
counts = {}
for line in sys.stdin:
evt = json.loads(line)
for key in evt.get('id', {}).keys():
counts[key] = counts.get(key, 0) + 1
for k, v in sorted(counts.items(), key=lambda x: -x[1]):
print(f'{v:6d} {k}')
"You will see a breakdown of the event types Bazel published—targets configured, published actions, test results, metrics, and more. The file is a structured invocation record, not a replacement for every other telemetry source.
From here, use BEP to inspect outcomes and tests, then add the source that fits the question: a profile for critical paths, execution logs for action-key differences, and remote-execution or infrastructure telemetry for queue, worker, storage, and cost analysis.
Going Deeper
This post covers the "what" and "why" of BEP. For event types, transports, upload behavior, ingestion, and data-source boundaries, read Bazel BEP Integration Guide: Events, Uploads, and Observability.
For the broader context of why build telemetry matters and what you can do with it, see What Is Build Observability?
If you want to skip building the ingestion layer, point your --bes_backend at Hermetiq and correlate BEP with the additional telemetry needed for cache, performance, and remote-execution diagnosis.
Hermetiq turns BEP into answers.
Stream Bazel build events to Hermetiq and connect invocation, target, test, cache, profile, and remote-execution evidence for engineers and AI agents. See the product.
Related Articles
- Bazel BEP Integration Guide — Event types, protobuf transports, ingestion, production pitfalls, and evidence boundaries.
- What Is Build Observability? — The metrics, logs, and traces that give you visibility into what happens inside your Bazel builds.
- How to Debug Bazel Remote Cache Misses — Compare execution logs to identify the key-forming fields that changed.