← Back to Blog

TL;DR: Every time you run a Bazel build, Bazel emits a structured stream of events called the Build Event Protocol (BEP). It contains everything that happened during the build: which targets succeeded, which actions ran, what hit the cache, how long each step took, and what tests passed or failed. BEP is not a log file. It is structured, machine-readable telemetry — and it is the foundation of every build observability tool in the Bazel ecosystem.

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 hundreds or thousands of actions, checked a remote cache for each one, decided what needed to execute locally versus remotely, ran tests in parallel, and aggregated results. All of that work produced information — timing data, cache hit rates, action strategies, test outcomes, resource consumption — and almost none of it is visible in the terminal output.

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. And that is exactly what BEP provides.

What BEP Actually Is

The Build Event Protocol is a structured stream of protobuf-encoded events that Bazel emits during every build invocation. It is not a feature you opt into — Bazel generates these events internally regardless. The question is simply whether you capture them.

Each event represents something meaningful that happened during the build: the build started, a target was configured, an action executed, a test produced a result, the build finished. Every event has a type, an ID, and a payload specific to that event type. Events reference each other through parent-child relationships, forming a directed acyclic graph that completely describes the build.

The key distinction is that BEP is not a log. Logs are unstructured text designed for human reading. BEP is structured data with a defined protobuf schema, typed fields, explicit relationships between events, and stable semantics across Bazel versions. It is telemetry — the kind of data you feed into dashboards, analytics pipelines, and alerting systems.

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.pb

After the build completes, that file contains every event Bazel produced. 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 --bes_results_url is optional — it causes Bazel to print a dashboard link at the start of every build so you can watch results in real-time. This is how tools like Hermetiq give you a live view of your build the moment it starts.

What You Get from BEP

The event stream is rich. Here are the key data points BEP gives you, and why each one matters:

  • Build duration and timestamps. Precise start and end times for the overall build, broken down by phase. Not the wall-clock guess from your terminal — exact microsecond timestamps.
  • Target outcomes. Every target that was configured, whether it succeeded or failed, and the output files it produced.
  • Action timing and strategies. For every action Bazel scheduled: what type it was (CppCompile, Javac, GoCompile, etc.), how long it took, and whether it was executed locally, remotely, or served from cache. This is the data behind cache hit rate metrics.
  • Test results. Per-test, per-shard, per-run outcomes. Pass, fail, flaky, timeout — with execution duration and log file references for each.
  • Cache hit/miss signals. Aggregated cache statistics and per-action cache behavior. The foundation for understanding and optimizing your remote cache.
  • Command line and options. The exact command that was invoked, including all resolved flags from .bazelrc files, command-line arguments, and defaults. Invaluable for debugging why two builds behaved differently.
  • Platform and configuration info. Which platforms were used, what toolchains were selected, and how targets were configured.
  • Build metrics summary. Total action count, CPU time, memory usage, and other aggregate statistics emitted at the end of the build.

To make this concrete, here is a simplified JSON representation of a single action event from the BEP stream:

{
  "id": {
    "actionCompleted": {
      "primaryOutput": "bazel-out/k8-fastbuild/bin/src/libutil.a",
      "label": "//src:util"
    }
  },
  "action": {
    "type": "CppCompile",
    "strategy": "remote cache hit",
    "exitCode": 0,
    "startTime": "2026-02-15T10:22:01.334Z",
    "endTime": "2026-02-15T10:22:01.337Z"
  }
}

That strategy field — "remote cache hit" versus "remote" versus "local" — is the single most important field for understanding build performance. Multiply it across every action in your build and you have a complete picture of cache effectiveness.

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 stable protobuf schema. Fields are typed. Relationships between events are explicit. The protocol is versioned. You do not need to write a regex to extract a cache hit rate — the data is in a structured field waiting to be read.

The practical consequence is reliability. A log-parsing pipeline will break when Bazel changes its output format, when a new flag alters verbosity, or when a build produces output that does not match expected patterns. A BEP-based pipeline survives Bazel upgrades because the protocol is the contract, not the terminal output.

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 like Hermetiq, BuildBuddy, and EngFlow all ingest BEP as their primary data source. When you point --bes_backend at one of these services, you are streaming BEP events to their ingestion pipeline. Every dashboard, metric, and insight they provide derives from BEP data.

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 at larger companies build custom dashboards and analytics on BEP data — tracking build health trends, identifying slow targets, measuring the impact of toolchain upgrades, and attributing remote execution costs to specific teams or repositories.

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_actions

The 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 every event type in the stream — how many targets were configured, how many actions completed, how many test results were produced, and more. That single file contains a complete, structured record of your build.

From here, you can start asking the interesting questions. Which actions took the longest? What was the cache hit rate? Which tests are slowest? The data is all there. The question is just how much tooling you want to build around it — or whether you want a platform that has already done that work.

Going Deeper

This post covers the "what" and "why" of BEP. If you want to go deeper into the technical details — the full event type taxonomy, the protobuf schema structure, how to build a BEP ingestion pipeline, and the common pitfalls when consuming BEP at scale — read our technical deep dive: Understanding Bazel's Build Event Protocol (BEP).

For the broader context of why build telemetry matters and what you can do with it, see What Is Build Observability?

And if you want to skip the build-it-yourself phase entirely — point your --bes_backend at Hermetiq and get instant dashboards, cache analytics, cost tracking, and AI-powered build debugging out of the box.

Hermetiq turns BEP into answers. Stream your Bazel build events to Hermetiq and get instant build observability: cache hit analytics, action timing breakdowns, test health tracking, and AI-powered debugging via MCP. No pipeline to build, no infrastructure to maintain. See the product.


Related Articles