TL;DR: The Build Event Protocol (BEP) is Bazel's structured event stream for invocation metadata, target and test outcomes, output references, failures, and aggregate build metrics. It is a core input to build observability—not a complete performance record by itself. For cache-root-cause analysis, critical paths, remote-execution phases, worker health, or cost attribution, combine BEP with execution logs, profiles, remote-execution telemetry, and infrastructure metrics.
Every time you run bazel build or bazel test, Bazel does a lot of work beneath the surface. It resolves the dependency graph, schedules actions, fetches cached results, executes what needs executing, and reports results. Most engineers interact with this process through terminal output — scrolling past lines of green and red text until they find the thing that broke.
But Bazel has a far richer interface for reporting the build lifecycle. It's called the Build Event Protocol, or BEP. It is a core input for build dashboards and CI integrations that need structured invocation, target, test, output, and failure data.
If you're running Bazel at any meaningful scale and you haven't engaged with BEP directly, this guide will walk you through what it is, what's in it, how to consume it, and what you can build on top of it.
What BEP Is
BEP is a stream of structured, protobuf-encoded events that Bazel publishes over the course of a build invocation. It represents selected lifecycle events—such as command start and finish, target outcomes, published actions, and test results—rather than every internal operation.
Each event has an ID, typed payload fields, and zero or more announced child event IDs. Those announcements form a directed acyclic graph (DAG) that describes the published stream and lets a consumer detect missing announced events; it is not a complete action or performance graph.
The protocol is defined in a set of protobuf schema files in the Bazel repository. The top-level message is BuildEvent, and the event type is determined by a oneof field covering all possible event kinds.
Why BEP Matters
The core reason BEP matters is simple: it is Bazel's structured interface to the build lifecycle. Without BEP, teams often parse console output—text designed for humans that can change between versions and mixes progress indicators with results.
BEP gives you machine-readable access to:
- The exact command that was run, including all flags and options
- Published target, configuration, and completion outcomes, including aborted work
- Failed-action details by default, or all
ActionExecutedevents when--build_event_publish_all_actionsis enabled - Test results, including per-shard and per-run outcomes
- Output file locations and their digests
- Invocation start, finish, CPU, wall-time, and other aggregate build metrics
This gives an observability system a reliable invocation backbone. It can answer what command ran, what targets and tests succeeded, and which failures Bazel reported; deeper performance diagnosis requires correlated telemetry.
Key Event Types
The BEP stream contains dozens of event types, but a handful carry the most analytical value. Here are the ones you'll interact with most.
BuildStarted
Emitted once at the beginning of every invocation. Contains the Bazel version, the command that was run (build, test, run, etc.), the workspace directory, and a start timestamp. This is your anchor event — it gives you the metadata you need to identify and correlate the invocation.
OptionsParsed
Contains the full set of startup options and command options that Bazel resolved for this invocation. This includes explicit flags from the command line, options from .bazelrc files, and defaults. Invaluable for understanding why a build behaved the way it did — especially when debugging differences between local and CI builds.
TargetConfigured and TargetComplete
TargetConfigured reports the target kind, test metadata, tags, and related configuration IDs. Separate Configuration events can expose fields such as mnemonic, platform name, and CPU. TargetComplete reports completion or failure and references output groups. Treat aborted events and partial builds explicitly.
ActionExecuted
One of the most information-dense events. As the official BEP glossary notes, Bazel publishes it for failed actions by default; enable --build_event_publish_all_actions to include successful actions too. It can contain the mnemonic, owner, exit code, stdout/stderr references, start and end times, command line, and optional strategy-supplied details. Consumers must not assume it contains complete cache or remote-execution diagnostics.
TestResult and TestSummary
TestResult is emitted per test shard per test run, with the pass/fail status, execution duration, and log file references. TestSummary aggregates across shards and runs for a given test target, including the overall status and total execution time. If you're tracking flaky tests or test performance trends, these are the events you need.
NamedSetOfFiles
Represents a set of output files, referenced by other events. This is how BEP handles the common case where multiple targets produce overlapping sets of outputs without duplicating the file metadata across events. It uses a tree structure — sets can reference other sets — which makes it memory-efficient but requires some care when traversing.
BuildFinished
Provides the authoritative command exit code and finish timestamp. It is not necessarily the final event: metrics and build-tool logs may follow. Consumers should use the announced event graph and BES lifecycle to determine stream completeness.
BuildMetrics
Emitted once at the end of the command with counters and gauges such as actions executed, target/package metrics, CPU and wall time, and—in some configurations—memory or action-cache statistics. These values summarize work Bazel reports for that invocation; they are not a complete per-action explanation of cached work.
How to Consume BEP
Bazel provides two primary mechanisms for consuming BEP events: writing to a local file, or streaming to a remote service.
Local file output
The simplest approach. Add the --build_event_binary_file flag to write the entire BEP stream to a local file in binary protobuf format:
bazel build //src:my_target \
--build_event_binary_file=/tmp/bep.pbYou can also use --build_event_json_file for a JSON representation, which is easier to inspect manually but significantly larger:
bazel build //src:my_target \
--build_event_json_file=/tmp/bep.jsonBinary protobuf is compact and preserves protobuf wire fidelity, including unknown fields that an older consumer may not understand. JSON follows protobuf's JSON mapping and is convenient for inspection, but it is larger and generally less future-proof. Choose based on your pipeline and version-compatibility requirements.
Remote streaming via BES
For production use, you typically don't want BEP written to local files. You want it streamed in real-time to a remote service that can ingest, store, and analyze it. This is what the Build Event Service (BES) protocol enables.
BES is a gRPC service that Bazel connects to at the start of a build. Bazel streams events to the BES backend as they're produced, and the backend acknowledges receipt. The relevant flags:
bazel build //src:my_target \
--bes_backend=grpcs://bep.example.com:443 \
--bes_results_url=https://dashboard.example.com/invocation/The --bes_backend flag tells Bazel where to stream events. The --bes_results_url flag is optional — it tells Bazel to print a URL at the start of the build where the user can view results in a dashboard. This is how build result UIs like Hermetiq provide a direct link to the invocation as soon as the build starts.
There are additional flags that control BES behavior:
# Upload BEP asynchronously (don't block build completion)
--bes_upload_mode=fully_async
# Set a timeout for BES uploads
--bes_timeout=60s
# Ask BES to validate earlier lifecycle events
--bes_check_preceding_lifecycle_eventsThe --bes_upload_mode=fully_async flag is worth calling out. It lets the command return without waiting for BES acknowledgements while upload continues; a later Bazel invocation waits for the pending upload. If the process is terminated, events may be lost and the stream may remain incomplete, so use the mode only when that reliability tradeoff is acceptable. The --bes_check_preceding_lifecycle_events flag instead asks BES to verify earlier invocation lifecycle events—it is not a backend-unreachable policy.
BEP vs. Logs: Structured vs. Unstructured
It's worth being explicit about why BEP is fundamentally different from parsing Bazel's console output — because many teams start with log parsing and wonder why it breaks.
Bazel's terminal output is designed for human readability. It uses colors, progress bars, and dynamic line rewriting. It truncates long output. It changes format between versions. It interleaves progress updates with errors in ways that make regex-based parsing fragile.
BEP, by contrast, is schema-defined and machine-readable. Events have typed fields and explicit relationships. The schema evolves and includes optional and deprecated fields, so consumers still need version-aware handling.
The practical difference is resilience. Console parsing can break when output formatting or verbosity changes; a BEP consumer can handle schema evolution explicitly and rely on defined field semantics instead of regexes.
If you're currently extracting build metrics from Bazel's stdout or stderr, consider this a strong signal to switch to BEP. The investment pays for itself the first time a Bazel upgrade doesn't break your dashboard.
What BEP Can—and Cannot—Tell You
The raw event stream is useful, but production diagnosis usually comes from joining BEP with other Bazel and infrastructure evidence. Keep the boundary explicit so dashboards do not overstate what a single source proves.
Invocation duration and lifecycle
BuildStarted, BuildFinished, and BuildMetrics provide authoritative command outcome and aggregate timing. Use a Bazel profile when you need critical-path and phase-level performance analysis.
Cache activity and miss diagnosis
BuildMetrics can provide aggregate action-cache statistics, and publishing all ActionExecuted events can add useful action evidence. To determine why an action key changed, compare Bazel execution logs from comparable builds; BEP does not contain the complete key-forming input set or prove an eviction.
BEP is a useful invocation anchor for cache miss debugging, while execution logs supply the action-key comparison evidence.
Test performance and flakiness
TestResult events across multiple invocations let you build a test performance history: which tests are getting slower, which tests fail intermittently, and which tests dominate your test execution time. Flaky test detection — where a test passes and fails on the same commit — requires exactly this kind of longitudinal BEP analysis.
Critical-path analysis
The critical path is the longest sequence of dependent work constraining the build. Bazel's profile is the appropriate source for critical-path and phase analysis; BEP can associate that evidence with an invocation but does not, by itself, expose the complete action dependency graph needed to reconstruct it.
Remote execution performance
When using remote build execution (RBE), correlate the BEP invocation with completed-action metadata and scheduler, worker, storage, and network telemetry. Those sources—not BEP alone—separate queueing, input fetch, execution, and output upload time.
Build cost tracking
If you're running remote execution on cloud infrastructure, cost attribution requires joining invocation and action identity with worker runtime, resource allocation, storage/egress, and provider billing data. BEP supplies useful invocation context, but it does not report complete worker consumption or infrastructure cost.
Practical Example: Inspecting a BEP Stream
To see what BEP events look like in practice, generate a JSON BEP file and inspect it:
bazel test //src/... \
--build_event_json_file=/tmp/bep.json
# Find the BuildStarted event
jq 'select(.id.started)' /tmp/bep.json
# Find published ActionExecuted events for a specific target
jq 'select(.id.actionCompleted.label == "//src:mylib")' /tmp/bep.json
# Inspect aggregate action metrics, when present
jq 'select(.id.buildMetrics) | .buildMetrics.actionSummary' /tmp/bep.json
# Get test results
jq 'select(.id.testResult)' /tmp/bep.jsonA deliberately partial ActionExecuted event can look like this; optional fields vary by action, Bazel version, and flags:
{
"id": {
"actionCompleted": {
"primaryOutput": "bazel-out/k8-fastbuild/bin/src/libmylib.a",
"label": "//src:mylib",
"configuration": { "id": "a1b2c3..." }
}
},
"action": {
"type": "CppCompile",
"exitCode": 0,
"startTime": "2026-02-21T14:30:01.123Z",
"endTime": "2026-02-21T14:30:04.567Z",
"stdout": { "uri": "bytestream://..." },
"stderr": { "uri": "bytestream://..." }
}
}Do not treat a made-up strategy string as a portable cache classifier. Current BEP action events expose typed fields plus optional strategy details; robust cache and execution analysis should use the fields actually emitted by your Bazel version and correlate them with execution logs or remote-execution telemetry.
Building a BEP Pipeline
If you're building a BEP ingestion pipeline from scratch, the typical architecture looks like this:
- Ingest. Run a gRPC server implementing the BES protocol. Bazel connects to it via
--bes_backendand streams events. Your server acknowledges each batch and writes the raw events to a durable store (object storage, Kafka, etc.). - Parse. Deserialize the protobuf events, honor announced event IDs, follow
NamedSetOfFilesreferences, and correlate records through IDs, labels, configuration IDs, and output references. Do not assume successful actions form a complete child set beneath each target. - Store. Write the parsed data to an analytical store — a time-series database for metrics, a search index for event lookup, or a data warehouse for long-term trend analysis.
- Correlate. Join the invocation with profiles, execution logs, completed-action metadata, and infrastructure metrics when answering performance, cache-root-cause, or cost questions.
- Query. Build dashboards, alerts, and APIs while preserving the provenance of each conclusion.
This is non-trivial to build and maintain. The BEP schema has subtleties — NamedSetOfFiles traversal is recursive, event ordering isn't strictly guaranteed, some events are only emitted for certain build commands, and the schema evolves across Bazel versions. Most teams that try to build this in-house end up with something that works for their specific use case but requires ongoing maintenance as Bazel evolves.
This is, not coincidentally, the core problem Hermetiq solves. We run the BES backend, handle the parsing and storage, and expose the derived analytics through dashboards and an MCP server that AI assistants can query directly.
Common Pitfalls
A few things to watch out for when working with BEP:
- Event ordering is not strictly sequential. Events are emitted as they become available, not in a guaranteed order. Your consumer must handle out-of-order delivery and use event IDs for correlation, not arrival order.
NamedSetOfFilesis a tree, not a flat list. A common mistake is to treat file sets as simple lists. They can reference other file sets, and you need to traverse the tree to get the full set of files.- Not all events are emitted in all modes. Some events are only produced during
bazel test, others only duringbazel build. TheActionExecutedevent is only emitted for failed actions by default — you need--build_event_publish_all_actionsto get events for successful actions too. - BEP streams can be large. A build with thousands of targets and tens of thousands of actions produces a substantial event stream. Your BES backend needs to handle this volume without back-pressuring Bazel and slowing down the build.
- JSON and binary have different tradeoffs. JSON follows protobuf's JSON mapping and is easy to inspect; binary is smaller and preserves protobuf wire and unknown-field fidelity. Choose deliberately and test schema evolution.
Getting Started with BEP
If you haven't worked with BEP before, start here:
- Generate a JSON BEP file for one of your builds with
--build_event_json_file. Open it in your editor and read through the events. Get a feel for the structure. - Look at the protobuf schema. The build_event_stream.proto file is the authoritative reference. It's well-commented and more readable than most protobuf schemas.
- Try the binary format with a simple protobuf reader in your language of choice. Parse the events, print the types, and get comfortable with the event DAG structure.
- Consider a managed BES backend. If you want the analytics without building the pipeline, Hermetiq ingests BEP and correlates it with the additional telemetry needed for cache, performance, and remote-execution diagnosis.
BEP is one of Bazel's most useful integration surfaces. It replaces fragile log scraping with structured build-lifecycle data and provides the invocation backbone for richer observability. Whether you build the pipeline yourself or use a managed solution, start by reading the events—and keep every derived conclusion tied to the source that actually supports it.
Hermetiq ingests BEP natively.
Point your --bes_backend at Hermetiq to preserve the BEP invocation record and correlate it with cache, profile, and remote-execution evidence for engineers and AI agents. See the product.
Related Articles
- What Is Bazel BEP? — A concise introduction before this implementation guide.
- How to Debug Bazel Remote Cache Misses — Compare execution logs to identify the key-forming fields that changed.
- Why Everyone Is Talking About MCP — Connect AI assistants to your BEP-powered build telemetry for instant root cause analysis.