B3 Propagation in OpenTelemetry

B3 Propagation in OpenTelemetry

How a small set of HTTP headers lets one request keep its identity as it hops across many services, languages and tools, and how OpenTelemetry reads and writes them so old and new systems can share one story.

Imagine sending a parcel that must pass through five courier companies before it reaches its owner. If each company invents its own tracking number and never tells the next one, you can see the parcel at each stop, but you can never see the whole journey. The parcel is real, the stops are real, yet the story is cut into five unrelated pieces. To follow it end to end, every company must pass along one shared tracking number, in a form the next company understands.

Software faces the same problem. A single click on a website can touch a dozen programs written in different languages by different teams. Distributed tracing is the tool that follows that click from start to finish. For it to work, each program must hand a small piece of identity to the next one. That hand-off is called propagation, and B3 is one of the most widely used formats for it. It was born in the Zipkin tracing system, and OpenTelemetry supports it so that older and newer systems can work together.

In this guide we begin from zero. We explain what tracing and propagation are, where B3 came from, and what every character of a B3 header means. Then we look at how OpenTelemetry reads and writes B3 inside your program, how to switch it on, how it compares with the newer W3C standard, and how to avoid common traps. Every new term is explained when it first appears, and each big idea comes with an everyday comparison.

1What Is B3 Propagation?

B3 is a small, agreed way of writing a trace’s identity into the headers of a request, so the next service can pick up the same trace.

To understand B3, we need three plain ideas. First, a request is one piece of work that a user or a program asks for, such as “show my shopping cart”. Second, a trace is the full record of everything that happened because of that one request, across every program that touched it. Third, propagation is the act of passing the identity of the trace from one program to the next, so that all the pieces can be glued together afterwards.

B3 is a propagation format. It says exactly which headers to use, what their names are, and what the values look like. A header is a small labelled note attached to a web request, like the address and stamps on an envelope. It travels with the request but is not part of the message itself. B3 uses these notes to carry a few facts: the identity of the trace, the identity of the current step, and whether the trace should be recorded.

Why the name B3?

The letter and number come from “BigBrotherBird”, the early working name of the Zipkin tracing project at Twitter. Three B’s, shortened to B3. Zipkin itself was inspired by a research paper from Google about a tracing system called Dapper. The name stuck even though the project grew and the old name faded. Today B3 simply means “the Zipkin-style way of passing trace identity”.

Two flavors of B3

B3 comes in two shapes. The multiple-header form spreads the facts over several headers, with names that start with X-B3-, such as X-B3-TraceId and X-B3-SpanId. The single-header form packs everything into one header named b3, with the facts joined by dashes. Both carry the same information. The single header was added later because it is shorter and easier to forward. We study both in detail in the next chapters.

Analogy

Think of the numbered tag on a piece of airline luggage. The tag carries a barcode that identifies the bag, and it follows the bag from the first airport to the last, even when the bag changes planes and airlines. The tag does not contain your clothes; it only lets every handler recognize the bag. B3 headers are the luggage tag of a request. They hold no business data, only the identity that lets every service say “this belongs to the same journey”.

What B3 is not

It is easy to mix B3 up with other parts of tracing, so let us be clear. B3 is not the data about what happened inside a service; that data is the spans, which are exported separately to a backend. B3 is not a monitoring tool or a storage system. B3 is not a naming dictionary like the semantic conventions, which decide what to call attributes. And B3 is not the only choice: the W3C Trace Context standard is the default in OpenTelemetry, and B3 is an alternative that you switch on when you need it.

i
Good to know

You only need B3 when something in your system already speaks B3, such as an older Zipkin-based service, a proxy configured for B3, or a library that defaults to it. If everything in your system is new and uses OpenTelemetry, the default W3C format is usually the simpler choice.

2The Problem: Broken Traces

Without propagation, a trace shatters into disconnected pieces, and the one view you wanted, the whole journey, disappears.

What happens without it

Picture a request that enters a gateway, is passed to an orders service, which calls a payment service and then a shipping service. Each service watches its own work and records spans. If nobody passes the trace identity along, each service starts a brand new trace when its request arrives. You end up with four small traces in your tool, each telling a fragment of the story, and no way to see that they all came from the same click.

This is called a broken trace or a set of orphan traces. The symptoms are easy to recognize. Dashboards show many traces that begin in the middle of the system. A slow request cannot be explained, because the slow part sits in a trace that nobody can link back to the user’s click. Engineers waste time guessing which payment trace belongs to which order.

flowchart LR
subgraph Without["Without propagation: four unrelated traces"]
G1["Gateway: trace A"] --> O1["Orders: trace B"]
O1 --> P1["Payment: trace C"]
O1 --> S1["Shipping: trace D"]
end
subgraph With["With propagation: one trace"]
G2["Gateway: trace A"] --> O2["Orders: trace A"]
O2 --> P2["Payment: trace A"]
O2 --> S2["Shipping: trace A"]
end
Figure 1: The same four services, once with broken traces and once with one shared trace identity.

Four kinds of pain

Pain 1

No end-to-end view

You cannot see how long the whole request took or which service was the slowest link in the chain.

Pain 2

Slow debugging

Engineers must match timestamps and guess which pieces belong together. This is slow and often wrong.

Pain 3

Wrong sampling

If services decide separately which traces to keep, you may keep the payment piece and lose the gateway piece of the same request.

Pain 4

Mixed formats

Services speak different propagation formats, so even when each one propagates, they cannot understand each other.

The fourth pain is where B3 matters

Propagation only works if the sender and the receiver agree on the format. If service A writes its identity in the B3 headers, but service B only looks for W3C headers, service B sees nothing and starts a new trace. Both services are doing their job, yet the trace breaks at the border. This is the most common reason for broken traces in real companies, and it is exactly the situation where OpenTelemetry’s B3 support is valuable.

Analogy

Two people can only pass a note if they write in a language both can read. If one writes in English and the other reads only French, the note arrives but nothing is understood. A propagation format is the shared language of the note. B3 and W3C Trace Context are two such languages, and a service that speaks both can talk to everyone.

1
trace identity shared by every step of one request
2
shapes of B3: multiple headers or a single header
0
business data inside the headers; only identity and sampling
!
Watch out

A broken trace gives no error. Every service looks healthy, every request succeeds, and nothing in your logs complains. You only notice when you look for the full picture and find it missing. That is why we later cover checks that detect broken traces early.

How the pain grows with your system

With two services, a broken trace is an annoyance. With two hundred services, it is a serious risk. A single request in a mature system can touch ten or twenty services, and every border between two services is a place where propagation can fail. If each border has even a small chance of breaking, then the chance that a long chain stays whole drops quickly. This is why large organizations put so much effort into agreeing on one propagation format, and why mismatched formats are one of the first problems they solve.

The cost also shows up in people’s time. When an outage is in progress, engineers rely on traces to find the broken piece quickly. A trace cut in the middle forces them back to reading raw logs and comparing clocks, which can add many minutes to every incident. Fixing propagation early is cheap, and it pays back during the first major outage that it shortens.

i
Good to know

Propagation works only if every border cooperates. One service that drops the headers is enough to break all traces that pass through it, no matter how well the other services are configured.

3A Quick Refresher: IDs, Context and Carriers

Six small terms explain almost everything about how propagation works: trace id, span id, parent, context, carrier and propagator.

The identity pieces

A trace id is a long random number that names the whole journey. Every span in the same trace carries the same trace id. A span is one timed step of work, like “handle the web request” or “call the payment service”. Each span has its own span id, another random number, which names just that step. A span that was started because of another span points to it as its parent. These parent links turn a pile of spans into a family tree.

Identifiers are written as strings of hexadecimal characters. Hexadecimal is a way of writing numbers with sixteen symbols: the digits 0 to 9 and the letters a to f. A 128-bit trace id becomes 32 such characters, and a 64-bit span id becomes 16 characters. You do not need to read them as numbers. Treat them like the long codes printed on a parcel label.

Context: the identity inside one program

Inside a single running program, OpenTelemetry keeps track of “which span am I in right now” using something called the context. The context is an invisible object that follows your code as it runs, so that any new span you start knows who its parent is. It does this work inside one process, without any headers.

Carrier and propagator: crossing the border

The trouble starts when the request leaves the program. The context lives in memory and cannot travel over a network by itself. So OpenTelemetry uses two helpers. The carrier is the thing that moves between programs, such as the headers of an HTTP request or the properties of a queue message. The propagator is the translator that writes the context into the carrier and reads it back out on the other side.

A propagator has exactly two jobs, given plain names. Inject means taking the current context and writing it into the carrier before sending. Extract means reading the carrier on arrival and rebuilding the context. A B3 propagator is simply a propagator that performs these two jobs using the B3 header format.

TermPlain meaningEveryday comparison
Trace idNames the whole journeyA parcel tracking number
Span idNames one step of the journeyThe scan at one depot
ParentThe step that caused this stepThe depot the parcel came from
ContextWhere I am right now, inside one programThe note in a worker’s pocket
CarrierThe thing that crosses the networkThe envelope
PropagatorWrites and reads the context into the carrierThe clerk who fills in the label and reads it
sequenceDiagram
participant A as Service A, the caller
participant B as Service B, the receiver
A->>A: Start a CLIENT span with the current context
A->>A: Inject trace id and span id into the carrier
A->>B: Send the request with B3 headers
B->>B: Extract the headers and rebuild the context
B->>B: Start a SERVER span as a child of the caller
B-->>A: Send the response
Figure 2: Inject on the sending side, extract on the receiving side. The propagator does both.
Analogy

Think of a relay race. The baton is the context, because it says which team you are on and which leg of the race you have reached. A runner cannot hand over the baton by shouting; they must physically pass it. The carrier is the hand-over itself, and the propagator is the agreed way of doing it: how to hold the baton out and how to take it. If two runners practice different hand-over styles, the baton drops. B3 is one hand-over style.

i
Good to know

OpenTelemetry also has baggage, a separate set of business key and value pairs that can travel with a request. Baggage uses its own propagator and is not part of B3. B3 carries only trace identity and the sampling decision.

Where the ids come from

A trace id must be unique across every request your whole system will ever handle, and it must be created without asking any central office, because services create traces at the same moment in many places. The solution is randomness. OpenTelemetry fills a trace id with random bits, and a 128-bit random number is so large that two requests picking the same one by accident is, for all practical purposes, impossible. A 64-bit id is smaller, so clashes become possible in very large systems over long periods, which is one reason that 128-bit ids are preferred today.

Randomness has a second benefit. Some samplers decide whether to keep a trace by looking at the trace id itself. A ratio sampler that wants to keep ten percent of traces can treat the id as a number and keep the trace if the number falls in a certain range. Because the id is random, this keeps about ten percent, and because every service looks at the same id, they all make the same choice. This gives consistent sampling even without a header carrying the decision. Ids that are not random, such as ones that count upward, would spoil this and should be avoided.

i
Good to know

Span ids are random too, but they only need to be unique inside their own trace, so a 64-bit span id is enough. That is why trace ids can be 32 characters while span ids stay at 16.

4A Short History of B3

B3 was a practical answer written by a team that needed it, long before any official standard existed.

From Dapper to Zipkin

In 2010, Google published a paper describing Dapper, the internal system it used to trace requests across thousands of machines. The paper showed that following a request through many services was both possible and valuable. Engineers at Twitter, who ran a large collection of services of their own, built an open implementation of the same idea. They named it Zipkin, and it was released as open source in 2012. The early code name, BigBrotherBird, gave B3 its name.

Zipkin needed a way to pass trace identity between services, so its authors defined a set of HTTP headers. At the time there was no standard for this, so the Zipkin headers became a de facto standard, which means a standard by common use rather than by official decision. Because Zipkin was open source and popular, many other tools adopted the same headers.

The ecosystem that grew around B3

Tracer

Brave

The Java tracing library from the Zipkin project. It uses B3 by default, and many Java services built on it still send B3 today.

Framework

Spring Cloud Sleuth

A long-used tracing layer for Spring applications. It built on Brave and used B3 by default. Newer Spring versions moved to Micrometer Tracing, which commonly uses W3C by default.

Proxy

Envoy and service meshes

The Envoy proxy supports Zipkin and B3 style propagation. Service meshes built on Envoy, such as Istio, historically used B3 headers in their tracing setups.

Library

Finagle and other Twitter-born tools

Libraries from the same family as Zipkin naturally speak B3, because they were built together.

Then came a W3C standard

As distributed tracing spread, the industry saw the cost of having many incompatible header formats. The World Wide Web Consortium, called W3C, wrote a common standard named Trace Context. It became an official W3C Recommendation in early 2020. It defines headers named traceparent and tracestate. OpenTelemetry chose W3C Trace Context as its default propagation format, because a neutral standard serves everybody better than the habit of one project.

B3 did not disappear. Millions of running services already sent it, and rewriting them all at once was not possible. OpenTelemetry therefore ships B3 propagators, so that a modern OpenTelemetry service can sit in the middle of an older B3 estate and keep the traces connected. This role, a bridge between generations, is the main reason B3 still matters.

1

2010

Google describes Dapper, proving that tracing across services is practical.

2

2012

Twitter open-sources Zipkin and its B3 headers.

3

2019

OpenTracing and OpenCensus merge to form OpenTelemetry, a project under the Cloud Native Computing Foundation.

4

2020

W3C Trace Context becomes an official Recommendation and later the OpenTelemetry default.

5

Today

B3 lives on in older estates and is supported in OpenTelemetry for compatibility.

Analogy

Think of railways. Early railways were built by different companies, and each chose its own width between the rails. Trains could not cross from one network to the other. Later a standard width was agreed, but old lines kept running on the old width for decades, and stations needed special equipment to move trains between them. B3 is an old rail width that is still widely in use, and the B3 propagator is the equipment at the junction.

5The Multiple-Header Format

The original B3 uses a handful of separate headers, each carrying one fact. Let us read them one at a time.

The five headers

HeaderWhat it carriesAllowed values
X-B3-TraceIdThe identity of the whole trace16 or 32 lowercase hexadecimal characters
X-B3-SpanIdThe identity of the sender’s current span16 lowercase hexadecimal characters
X-B3-ParentSpanIdThe parent of the sender’s current span, when there is one16 lowercase hexadecimal characters
X-B3-SampledWhether this trace should be recorded1 for yes, 0 for no
X-B3-FlagsExtra switches, in practice only the debug switch1 means debug

Reading each header

X-B3-TraceId is the heart of the format. It is either 16 characters, which is a 64-bit number, or 32 characters, which is a 128-bit number. The 64-bit form comes from the early days of Zipkin. The 128-bit form is longer and makes accidental clashes between traces practically impossible, and it matches the size used by W3C Trace Context. Many systems accept both, and a correct receiver must handle either.

X-B3-SpanId names the span of the sender at the moment of sending. When the receiver starts its own span, it will give it a fresh span id, and it will treat the sender’s span id as the parent. This is how the family tree is built one hand-over at a time.

X-B3-ParentSpanId is optional. It names the parent of the sender’s span. The receiver does not need it to build a child span, because the sender’s span id is already enough. The header exists mainly for tools in the Zipkin style, where both sides of one call may share a span id, and it is often left out by newer implementations.

X-B3-Sampled tells the receiver whether to record the trace. A value of 1 means “accept, record this trace”. A value of 0 means “deny, do not record”. If the header is missing, the sender has made no decision, and the receiver is free to decide for itself. We look at this important idea in the sampling chapter.

X-B3-Flags is a leftover switch from the early design. The only meaningful value in practice is 1, which means “debug”. A debug trace is a request that must be recorded no matter what the normal sampling rules say. It is useful when an engineer wants to be sure that one specific request is captured.

A worked example

Suppose a gateway sends a request to the orders service. It writes the following facts into the headers, shown here in a table so that each part is clear.

HeaderExample valueMeaning
X-B3-TraceId80f198ee56343ba864fe8b2a57d3eff7The 128-bit identity of the trace
X-B3-SpanIde457b5a2e4d86bd1The gateway’s span, which becomes the parent
X-B3-Sampled1Record this trace

The orders service extracts these values. It starts a new span with a new span id of its own, keeps the same trace id, and records the gateway span as its parent. Because the sampled value is 1, it records the span too. When orders calls payment, it injects the same trace id, its own span id and the same sampling choice, and the chain continues.

Analogy

The multiple headers are like a paper form with separate boxes: one box for the tracking number, one for the current depot, one for “should we keep records of this parcel”. Each box is easy to read and fill in on its own, but there are several boxes to copy at every hand-over. The single-header format, coming next, is the same form folded into one line.

!
Watch out

Header names are not case sensitive in HTTP, so X-B3-TraceId and x-b3-traceid mean the same thing. Some systems, such as gRPC, require lowercase names and will send them that way. A correct implementation compares names without regard to case. The hexadecimal values themselves should be written in lowercase.

What makes a header valid

A careful receiver checks the values before using them, because a bad id can cause strange behavior later. The rules are short. The trace id must be exactly 16 or 32 characters. The span id must be exactly 16 characters. Every character must be a hexadecimal digit, and the letters should be lowercase. Finally, an id made only of zeros is invalid, because zero is reserved to mean “no id”. If any rule fails, a well-behaved propagator ignores the whole set of B3 headers and treats the request as if it had arrived without them.

Ignoring the headers, rather than guessing, is deliberate. Guessing could glue a request to the wrong trace and create misleading pictures. Starting a new trace is safer, because the result is merely an incomplete story and not a false one. The same caution is why you should not repair malformed ids by hand in your own code.

SituationWhat a good receiver does
All headers validContinue the same trace as a child span
No B3 headers at allStart a new root trace
Trace id has the wrong lengthIgnore the headers and start a new trace
Trace id is all zerosIgnore the headers and start a new trace
Sampled value is something other than 1 or 0Ignore the sampling value, or the whole set, depending on the library
Only a sampled value of 0 in the single headerHonor the denial and do not record
Analogy

A security guard checking tickets does not guess what a smudged ticket was meant to say. A smudged ticket is simply not accepted, and the visitor is treated as new. A receiving propagator behaves like that guard: it accepts only clean, complete identity, and otherwise starts fresh.

6The Single-Header Format

The single header says the same things as the five older headers, folded into one short line.

Why a second format was invented

Passing five headers at every hop works, but it has costs. Each header must be copied by every proxy, every library and every message system along the way. Some messaging systems, and some gateways, handle only a limited number of headers. Forgetting just one of the five can break the trace. The Zipkin community therefore added a compact form, named simply b3, which carries all the same facts in one value.

The shape of the value

The value is made of up to four parts joined by dashes, always in the same order: the trace id, the span id, the sampling state and the parent span id. The first two parts are required when the header carries identity. The last two are optional.

PositionPartRequired?Notes
1TraceIdYes16 or 32 lowercase hexadecimal characters
2SpanIdYes16 lowercase hexadecimal characters
3SamplingStateNo1 for accept, 0 for deny, d for debug
4ParentSpanIdNo16 hexadecimal characters; often left out

With the same values as our earlier example, the header would read: 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1. The three parts are the trace id, the span id and the sampling state of 1. If the sender also wants to include the parent, a fourth dash-separated part follows the sampling state. Notice that the parent comes last, and so it cannot be written without also writing the sampling state.

A special short form

A sender may want to pass only a sampling decision without any identity. For example, a service that decided not to trace a request can simply send the value 0 in the b3 header. This tells the next service “do not record this one”. Because there is no trace id in this short form, the receiver does not link anything to a parent, and it respects the decision to stay quiet.

Analogy

The multiple headers are a form with several boxes. The single header is the barcode printed along the bottom of the same form. A barcode scanner reads the whole thing in one sweep, and it is easy to copy to the next page without missing a box. The price is that you must know the order of the fields to read it by eye.

Single or multiple: how to choose

Single header

  • Short and easy to forward.
  • Fewer chances to lose one piece on the way.
  • Friendly to systems with limited header support.
  • Newer, so some very old tools do not read it.

Multiple headers

  • Understood by almost every Zipkin-era tool.
  • Easy to read by eye in logs.
  • More headers to copy at every hop.
  • Higher risk that a proxy strips only some of them.

In practice, the choice is made by what your receivers understand. If you control every service and they all read the single header, use it. If older tools are in the path, the multiple-header form is the safe choice. Many OpenTelemetry propagators can read both formats automatically, and the choice you must make is only about what to write.

i
Good to know

Senders should write one B3 format, not both at once, unless a migration needs it. Receivers should be able to read either. This simple rule, “be strict when writing and generous when reading”, prevents many surprises.

7Sampling and Debug in B3

B3 does more than pass an identity. It also carries a decision: should this trace be recorded at all?

What sampling means

A busy system can handle thousands of requests every second. Recording every trace in full would cost a great deal in storage, network and money. So most systems record only a fraction of traces. Choosing which ones to keep is called sampling. The important rule is that the choice should be made once, usually at the start of a request, and then followed by every service. If each service chose on its own, you would get a trace with some steps present and others missing.

The three states

B3 expresses the decision with three possible situations. Accept means “record this trace”, written as 1. Deny means “do not record it”, written as 0. Deferred means that no decision has been made yet, and it is shown by leaving the sampling part out. When a receiver sees a deferred state, it is free to make the choice for itself using its local sampling rules, and then it passes its decision to the next service.

flowchart TD
A["Request arrives with B3 headers"] --> B{"Is a sampling value present?"}
B -- "Yes, 1 or d" --> C["Record the trace"]
B -- "Yes, 0" --> D["Do not record the trace"]
B -- "No" --> E["Local sampler decides"]
E --> F["Pass the decision to the next service"]
C --> F
D --> F
Figure 3: How a receiving service uses the sampling part of B3.

How OpenTelemetry honors the decision

In OpenTelemetry, a sampler is the component that decides whether a new span is recorded. The commonly used parent-based sampler follows a simple rule: if the incoming request comes from a remote parent that was sampled, record this span too; if the remote parent was not sampled, do not record it; and if there is no parent, use a chosen rule such as “keep ten percent”. When the B3 propagator extracts a header, it translates the accept value into the same “sampled” flag that W3C uses, so the sampler works the same way for both formats.

Analogy

Consider a school photographer who photographs some of the students at each assembly. Deciding independently at every row would give a photo with a random mix of rows missing. A better method is for the front row to announce, “we are taking the photo today”, and every other row follows. The sampling state in B3 is that announcement passed from row to row.

The debug state

Sometimes an engineer wants one particular request to be recorded for certain, whatever the normal rate. The debug state exists for this. In the multiple-header form it is written as X-B3-Flags with the value 1, and in the single-header form as the letter d in the sampling position. A debug request implies that the trace is accepted. Debug is treated as sampled by OpenTelemetry propagators, and some implementations also remember that debug was requested. Check your language’s package for the exact behavior.

1
accept: record this trace
0
deny: do not record this trace
d
debug: record it no matter what

Head sampling and tail sampling

The decision carried by B3 is a head sampling decision, made at the head, or start, of the trace before anything is known about how it ends. Another approach, tail sampling, waits until the trace is complete and then keeps the interesting ones, such as the slow or failed ones. Tail sampling is usually done in a Collector, and it does not depend on B3 flags. The two methods can work together, but remember that if the head decision was “deny”, no spans were recorded and a tail sampler cannot bring them back.

!
Watch out

Never let anyone on the public internet force the debug state on your system. If outside callers can send d or a sampled value of 1 freely, they can make you record every request, which raises your costs and may let attackers flood your tracing backend. Edge services should reset or ignore sampling values from untrusted sources.

Sampler choices and how B3 interacts

OpenTelemetry offers a few ready-made samplers. Always on records everything. Always off records nothing. A ratio-based sampler records a chosen percentage of traces. The parent-based sampler wraps another sampler for the root decision and then follows the remote parent for every later span. The most common production setup is a parent-based sampler wrapped around a ratio-based one: the first service in the chain decides using the ratio, and everyone else follows.

The B3 sampling value plugs straight into this design. When the first service decides to record, it injects the accept state. When a downstream service extracts it, the parent-based sampler reads it as “remote parent sampled” and records the span. When the first service decides not to record, it injects the deny state, and the downstream services stay quiet. The whole chain behaves as one, which is exactly the goal.

Incoming B3 stateRemote parent viewTypical result with a parent-based sampler
Accept, value 1SampledSpan is recorded
Debug, value d or flags 1Sampled, with debug notedSpan is recorded
Deny, value 0Not sampledSpan is not recorded, but context still passes on
No sampling valueTreated according to the libraryLocal rule decides, or the library may treat it as not sampled

That last row deserves attention. Libraries differ in how they treat a deferred decision when there is a valid trace id and span id but no sampling value. Some treat it as not sampled, and some let the local sampler decide. Read the documentation of your language package, and test the behavior with a real request rather than assuming.

!
Watch out

A service that is “not sampled” must still pass the context onward, even though it records nothing. If it drops the headers instead, a later service may start a fresh trace and decide to record it, giving you traces that begin in the middle of the system.

8How It Works Inside OpenTelemetry

Follow one request step by step to see exactly when B3 headers are written and read.

The propagator interface

In every OpenTelemetry language, a text-based propagator offers the same small set of abilities. Inject takes a context, a carrier and a setter, and writes values into the carrier. Extract takes a context, a carrier and a getter, and returns a new context that includes what it found. A third ability, often called fields, lists the header names the propagator may write, so that other tools know which headers to allow or forward.

The setter and getter exist because carriers differ. An HTTP request, a gRPC call and a message on a queue all store their labels in slightly different ways. By giving the propagator a small helper that knows how to write one label or read one label, OpenTelemetry lets the same B3 code work in every setting.

The life of one request

1

The caller starts a CLIENT span

The HTTP client library creates a span for the outgoing call. This span becomes the current span in the context.

2

The B3 propagator injects

It reads the trace id, span id and sampled flag from the context and writes them to the request headers in the chosen B3 form.

3

The request crosses the network

Proxies and gateways in between should forward the headers untouched, as they do with any other header.

4

The receiver extracts

The server library hands the incoming headers to the propagator. It rebuilds a remote span context holding the trace id, the span id, the sampled flag and a mark that says it came from another process.

5

The sampler decides

The parent-based sampler looks at the remote span context and records or skips the new span accordingly.

6

A SERVER span starts as a child

The new span gets its own span id, keeps the trace id, and points to the caller’s span as parent. Work inside the receiver now hangs under it.

flowchart LR
Ctx1["Context in caller"] --> Inj["B3 propagator: inject"]
Inj --> Hdr["Carrier: HTTP headers"]
Hdr --> Ext["B3 propagator: extract"]
Ext --> Ctx2["Context in receiver"]
Ctx2 --> Span["New child span"]
Figure 4: The context is written into headers, crosses the network, and is rebuilt on the other side.

What if the headers are missing or wrong?

A good propagator is forgiving. If the headers are missing, extract simply returns the context unchanged, and the receiver starts a new root trace. If the headers are present but malformed, for example a trace id of the wrong length or with letters outside a to f, the propagator ignores them rather than failing the request. This protects your application from crashing because of a bad header, but it also means a typo on the sending side quietly produces a broken trace.

The composite propagator

Most real configurations use several propagators together. OpenTelemetry offers a composite propagator, which runs a list of propagators in order. On inject, it asks each one to write its own headers, so the request may carry both W3C and B3 headers. On extract, it asks each one in turn to read what it understands. The order can matter if different headers disagree, so keep the list short and test the result.

Analogy

A composite propagator is a clerk who writes the address on the parcel in two alphabets, and who reads a label in whichever alphabet is present. If the label has only one alphabet, the clerk reads that one. If it has two, the clerk reads them in a fixed order. This helps parcels cross borders but needs a little care when two alphabets give different addresses.

i
Good to know

You almost never call inject and extract yourself. Instrumentation libraries for web servers, HTTP clients and message systems do it for you at the right moments. You only handle them by hand when you use a transport that has no instrumentation, such as a custom protocol.

Where inject and extract are called

Different pieces of instrumentation call the propagator at different points. A web server library calls extract when a request first arrives and before it creates the server span. An HTTP client library calls inject just before it sends a request, after it has created the client span. A messaging library calls inject when a message is produced and extract when a message is consumed. Each of these libraries is pre-wired to the propagator that you configured globally, so one configuration change affects all of them together.

This global behavior is helpful but means you should set the propagators early, before the first request is handled. If a service starts handling traffic with the default list and only later switches to include B3, the early requests will have been processed with the wrong format.

When context gets lost inside a program

Propagation between programs is only half of the story. Inside one program, the context must also travel correctly from one piece of code to the next. In languages that use threads, thread pools or asynchronous callbacks, the context can be lost when work is handed from one thread to another. When that happens, the outgoing call is made with an empty context, and the B3 propagator has nothing to inject. The result looks exactly like a missing header problem, but the cause is inside your own code.

The remedy is to use the context-aware tools of your language and OpenTelemetry instrumentation, which wrap executors, tasks and callbacks so that the context follows the work. If you write a custom thread pool, make sure the context is captured when the task is submitted and restored when it runs.

Example: the vanishing parent

A service receives a request with valid B3 headers and records a SERVER span correctly. It then hands the work to a background thread pool that calls a downstream service. The downstream service sees no B3 headers at all. The headers were extracted correctly, but the background thread did not inherit the context, so inject had nothing to write. Wrapping the thread pool with the OpenTelemetry context helpers solved the problem.

9Turning On B3 in OpenTelemetry

Switching on B3 is mostly a matter of choosing a propagator by name and adding a small package.

The standard setting

OpenTelemetry defines an environment variable called OTEL_PROPAGATORS. Its value is a comma-separated list of propagator names. If you do not set it, the default is tracecontext,baggage, which means W3C Trace Context plus baggage. To add B3 you put its name in the list.

NameWhat it does
tracecontextW3C Trace Context headers, traceparent and tracestate
baggageW3C Baggage, for business key and value pairs
b3B3 in the single-header form
b3multiB3 in the multiple-header form
jaegerThe Jaeger format, using the uber-trace-id header
xrayThe AWS X-Ray format, using X-Amzn-Trace-Id
ottraceThe older OpenTracing format
noneNo propagation at all

Choosing b3 makes the SDK write the single header when sending. Choosing b3multi makes it write the five-header form. In both cases the propagator understands incoming B3 headers. A typical bridge setting that talks to both generations is the list tracecontext,baggage,b3, which writes W3C and single-header B3 together.

Language packages

The B3 propagator is usually shipped as an additional package rather than inside the core SDK, because most users do not need it. The names vary by language, so look for the “B3 propagator” in the documentation of the language you use.

Java

Trace propagators extension

Provides a B3 propagator that can be created to write either the single header or multiple headers.

JavaScript

Propagator B3 package

Offers a B3 propagator with an option to choose between the single-header and multiple-header encoding.

Python

Propagator B3 package

Provides separate classes for the single-header and the multiple-header formats.

Go

Contrib propagators

The contrib collection includes a B3 propagator with options to select the encoding.

.NET

Extensions for propagators

An extensions package provides a B3 propagator alongside others such as Jaeger.

Auto setup

Agents and auto-configuration

Many auto-instrumentation agents read OTEL_PROPAGATORS and wire up the right propagator without code changes.

!
Watch out

Package names, class names and supported options change between versions. Always confirm them in the current documentation for your language and version before you rely on a specific name from memory.

A safe rollout order

1

Find out who speaks what

List each service and note which headers it sends and which it reads. Logs of incoming headers or a quick packet inspection can tell you.

2

Add the package

Install the B3 propagator package in the services that need to talk to B3 neighbours.

3

Set the propagator list

Use the environment variable with both formats, so the service reads and writes what its neighbours expect.

4

Check a real trace

Send one test request across the boundary and confirm in your tracing tool that it forms one trace with the right parents.

Testing your configuration

After you change the propagator list, test it before you trust it. A good test is simple and needs no special tools. Send one request into the first service in your chain, give it a recognizable path, and then look for the resulting trace in your tracing tool. If the setup is right, you will see one trace containing spans from every service on the path, with the correct parent and child links.

If something is wrong, work backwards one border at a time. At each service, check whether the incoming request carried the headers you expected, using request logging or a debugging proxy. Then check what the service sent onward. The place where the headers appear on the way in but not on the way out, or the place where they never appear on the way in, is the place to fix.

Test 1

One request, one trace

Confirm that a single request creates a single trace with all services present.

Test 2

Both formats

Send a request carrying only B3, and another carrying only W3C, and confirm that both are joined.

Test 3

Denied sampling

Send a request marked as not sampled and confirm that no spans are recorded downstream.

Test 4

Bad input

Send a request with a broken trace id and confirm that the service still answers normally and starts a new trace.

10B3 Compared with W3C and Others

B3 is one of several propagation formats. Seeing them side by side makes the trade-offs clear.

FormatHeader namesMain idea
B3 multipleX-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled, X-B3-FlagsOne fact per header, from the Zipkin project
B3 singleb3All facts in one dash-joined value
W3C Trace Contexttraceparent, tracestateAn official standard with room for vendor data
Jaegeruber-trace-idOne header, from the Jaeger tracing project
AWS X-RayX-Amzn-Trace-IdThe format used by the X-Ray service

B3 versus W3C Trace Context

These two formats solve the same problem, and they carry almost the same core facts, but they differ in important ways.

PointB3W3C Trace Context
OriginZipkin project, a common practice that grew by useW3C, an official Recommendation
Trace id size64 or 128 bitsAlways 128 bits
Sampling signalAccept, deny, debug, or deferred by leaving it outA sampled bit in the flags field
Vendor dataNo standard place for itThe tracestate header
OpenTelemetry defaultNo, it is switched on by choiceYes
Best fitZipkin-era and mesh-based estatesNew systems and cross-vendor traffic

W3C Trace Context gives every trace a 128-bit id and has a defined place, called tracestate, where each vendor can add its own information without clashing. B3 has no such place. B3 allows the deferred sampling state and a debug switch, which W3C does not model in the same way. When a 64-bit B3 trace id arrives at an OpenTelemetry service, the propagator extends it with leading zeros to reach 128 bits, so that the rest of the system sees a uniform size.

Analogy

B3 and W3C are like two postal address layouts. One was invented by a large local mail company and became familiar through use. The other was agreed by an international body and has a field for notes from each carrier. Both can deliver the same parcel. The international layout is easier when many carriers are involved, and the local layout is perfectly fine inside a region that already uses it.

i
Rule of thumb

Use W3C Trace Context for anything new and for traffic that crosses company boundaries. Use B3 where existing components already send it, and plan to move away from it over time.

The other formats in short

You may meet two more formats in older systems. The Jaeger format puts the trace id, span id, parent span id and flags into a single header named uber-trace-id, joined by colons. It comes from the Jaeger tracing project, which began at Uber and is now a CNCF project. The AWS X-Ray format uses a header named X-Amzn-Trace-Id with labelled parts separated by semicolons, and it is understood by the AWS tracing service and by many AWS-managed components. OpenTelemetry has propagators for both, selected by the names jaeger and xray in the propagator list.

The lesson is the same for all of them. The details differ, but each format carries a trace id, a span id and a sampling flag. A bridge service can read any of them and continue the same OpenTelemetry trace, which is the main promise of the propagator design.

i
Good to know

When you are not sure which format a neighbour sends, look at the header names. The names X-B3- and b3 mean B3, traceparent means W3C, uber-trace-id means Jaeger, and X-Amzn-Trace-Id means AWS X-Ray.

A decision guide for your propagator list

Your situationSuggested list
Everything is new and uses OpenTelemetrytracecontext and baggage, the default
Older Zipkin or Brave services call youtracecontext, baggage and b3
A mesh sends the multiple-header form and you must reply in kindtracecontext, baggage and b3multi
You live inside AWS X-Raytracecontext, baggage and xray, as your setup requires
Tracing must be off for a componentnone

11Living in a Mixed Estate

Real companies are rarely all old or all new. The B3 propagator earns its keep in the middle of that mix.

A typical mixed picture

Consider a company that began with Java services using Brave and Spring Cloud Sleuth, all writing B3. Over the years a service mesh was added, and its proxies also write B3 headers. Now new teams start building services with OpenTelemetry, which by default writes and reads W3C. If nothing is changed, the new services will not read B3 headers from the old ones, and the traces will break at every border.

flowchart LR
Legacy["Legacy Java service: sends B3"] --> Mesh["Mesh proxy: forwards B3"]
Mesh --> New["New OpenTelemetry service: reads B3 and W3C"]
New --> Newer["Newer service: sends W3C and B3"]
Newer --> Back["Tracing backend: one connected trace"]
Figure 5: A bridge service reads B3 from older neighbours and writes both formats forward.

The bridge rule

The cure is simple. Every service that sits between generations should read both formats and write the formats that its callees need. In configuration terms this usually means listing both W3C and B3 in the propagator setting. The bridge service extracts B3 from its older caller, continues the same trace, and injects both W3C and B3 for whatever it calls next.

Services that do no tracing at all

Not every program creates spans. Some are simple pass-through components: a gateway, a cache layer or a message router. These must still forward the tracing headers they received, because otherwise the trace breaks at that point. A service that has no tracing knowledge should copy all headers that begin with X-B3-, plus b3, plus traceparent and tracestate, onto its outgoing requests. This forwarding behaviour is a quiet but vital piece of the puzzle.

Example: the invisible gateway

A company has a gateway written ten years ago that strips every header it does not know. After the team adds OpenTelemetry to the services behind it, every trace starts at the first service behind the gateway, and the time spent in the gateway is lost. A one-line change to allow the tracing headers to pass restores the whole picture. The fix was in the gateway, not in OpenTelemetry at all.

Analogy

An international airport has staff who speak several languages at the transfer desk. Passengers arriving from any country are understood, and they are sent onward with a ticket written in the language of the next airline. The B3 propagator in a bridge service is that multilingual transfer desk.

!
Watch out

If both W3C and B3 headers arrive and they disagree about the trace id, the result depends on the propagator order. This can happen when two different systems along the path each start a trace. Keep the propagator list short, put the format you trust most first, and watch for traces that split in the middle.

12B3 Across Different Transports

B3 was born in HTTP, but modern systems also talk through gRPC, message queues and browsers. Each carrier has its own small rules.

Plain HTTP

HTTP is the easy case. Headers are free-form labels, names are not case sensitive, and every web library can read and write them. B3 works here with no special effort. The usual trouble is a proxy or gateway that removes headers it does not recognize, which we mentioned earlier.

RPC

gRPC

gRPC carries labels as metadata, and metadata keys must be lowercase. B3 headers are therefore sent as x-b3-traceid and similar, or simply as b3. Receivers must compare names without regard to case.

Queues

Message brokers

Systems such as Kafka and RabbitMQ let you attach headers or properties to each message. A producer injects B3 into those properties and a consumer extracts it. Some brokers store values as raw bytes, so the text must be encoded and decoded the same way on both sides.

Browser

Web pages calling APIs

When a page in a browser calls an API on another domain, the browser blocks custom headers unless the server lists them as allowed. The B3 header names must appear in the server’s allowed-headers list, or the call may fail or lose its tracing.

Jobs

Background and scheduled work

A job started later from a stored record must save the trace identity with the record if you want the job linked to the original request. The stored identity is then extracted when the job runs.

Messages are different from calls

A call is answered quickly, and the caller waits. A message may sit in a queue for minutes or hours, and many consumers may read it at different times. Because of this delay, a consumer often links its span to the producer’s span instead of nesting under it as a strict child. Propagation still uses the same B3 values, but the tracing convention for messaging explains when to use a parent and when to use a link. Check the messaging guidance in the semantic conventions for the details.

The browser trap in detail

The browser trap deserves extra attention, because it is one of the most common surprises. For security, a browser enforces a rule called cross-origin resource sharing, shortened to CORS. When your page at one address calls an API at another address, the browser first asks the API, “which request headers will you accept from me?” If the answer does not include the B3 names, the browser refuses to send them, or blocks the call. The fix is on the API server: add the B3 header names to its list of allowed headers. A trace that begins in the browser then flows into the backend.

Analogy

Different transports are like different ways of sending the same parcel label. A letter has an envelope, a courier van has a clipboard, and a locker has a screen. The label must be written onto each in the form that medium accepts. Whoever receives it must know where to look. B3 provides the contents of the label; each carrier decides where the label goes.

i
Good to know

If you use a standard OpenTelemetry instrumentation library for your transport, the getter and setter details are already handled. Only custom or unusual transports require you to write them yourself.

13Migrating from B3 to W3C Trace Context

Many companies use B3 today and plan to retire it. A careful migration keeps every trace connected on the way.

Why migrate at all

Staying on B3 forever is possible, but there are good reasons to move. W3C Trace Context is the OpenTelemetry default, so new libraries and tools support it first. It is an official standard, which helps when traffic crosses organization boundaries or reaches cloud services that understand it. It also has the tracestate header for vendor information. And the more formats your services must support, the more settings, tests and surprises you carry.

The key idea: overlap, then retire

You cannot switch every service at one moment. A service that only writes W3C would break its B3-only callees, and the reverse is also true. The safe path is to pass through a period when services speak both formats, so that old and new neighbours both understand them, and only then to remove B3.

flowchart LR
P1["Phase 1: all services use B3"] --> P2["Phase 2: read both formats everywhere"]
P2 --> P3["Phase 3: write both formats"]
P3 --> P4["Phase 4: stop writing B3"]
P4 --> P5["Phase 5: remove B3 support"]
Figure 6: Readers move first, then writers. B3 is removed only after nothing depends on it.
1

Make every service a reader of both

Before anyone writes W3C, every service must be able to read it. Enable both formats for extract everywhere. Because reading is harmless, this step is low risk.

2

Start writing both

Services begin sending W3C headers as well as B3. Each hop now works whichever format its neighbour prefers. Header size grows by a small amount.

3

Measure what still depends on B3

Count the incoming requests that carry only B3 headers. When that number reaches zero for every service, the old format has no readers left.

4

Stop writing B3

Remove B3 from the write side first, service by service, watching for broken traces after each step.

5

Remove B3 support

When no service reads B3 anymore, remove the package and the configuration. Update mesh and gateway settings too.

Things to remember during the move

Include the infrastructure in your plan, not just the application code. Service mesh proxies, API gateways, ingress controllers and load balancers all have tracing settings, and they may default to B3. Include third-party services that call you or that you call. Keep a rollback ready: because you add W3C before removing B3, you can usually undo a step by turning B3 writing back on.

i
Good to know

Do not hurry to the last step. The cost of sending a few extra bytes per request is tiny compared with the cost of a week of broken traces. Leave the overlap phase in place until the numbers prove that nothing needs B3.

Analogy

When a country changes the side of the road that cars drive on, it cannot do so one car at a time. Planners prepare signs and training first, then switch everyone together. In software we cannot switch everyone together, so we create a long period when both rules are understood. Only when almost nobody uses the old rule do we remove its signs.

14Security, Monitoring and Cost

B3 headers look harmless, but they cross trust boundaries. A little care keeps them safe, visible and cheap.

Security: treat incoming headers as untrusted input

Any caller on the internet can send any header. A service that blindly trusts the B3 headers it receives from the public can be abused in three ways. First, an attacker can send a sampled value of 1 or the debug state on every request, forcing you to record everything and increasing your storage bill. Second, an attacker can send malformed values in the hope of crashing a poorly written parser, which is why propagators must ignore bad input rather than fail. Third, an attacker can reuse a known trace id to pollute a real trace with fake spans.

Defense

Reset at the edge

At the public entry point, ignore incoming sampling values and trace identity from untrusted callers, and start fresh traces with your own decision.

Defense

Limit debug

Allow the debug state only from trusted internal tools or authenticated engineers, never from the open internet.

Defense

Validate strictly

Check lengths and characters, and ignore anything that does not match the format. Do not echo header values into logs without cleaning them.

Defense

No secrets in ids

Trace ids should be random. Never build them from user names, emails or account numbers, since they appear in many logs and tools.

Monitoring: spot broken traces early

Because broken traces make no noise, you need to look for them. A helpful signal is the share of traces whose first span is not a true entry point. If a service that should always have a parent suddenly shows many root spans, its extract step is failing. Another signal is the count of requests that carry B3 headers but produce no child span. A third is a regular test request, sometimes called a synthetic check, that crosses several services and verifies that the trace appears as one tree.

Cost: small, but not zero

The B3 single header is roughly fifty to one hundred bytes, depending on whether the ids are 64 or 128 bits and whether the parent is included. Writing both B3 and W3C together adds a few hundred bytes at most. For ordinary traffic this is negligible, but a system handling millions of requests per second with tiny payloads may notice it. The larger cost risk is the one in the previous chapter: forced sampling. Controlling who can set the accept and debug states controls most of the real cost.

~100 B
typical size of the single b3 header
32
hexadecimal characters in a 128-bit trace id
16
hexadecimal characters in a span id
!
Watch out

Do not log full header values from untrusted sources without limits. A malicious caller could send a very long value to fill your logs. Keep a maximum length, and log only validated ids.

A small health dashboard for propagation

You can turn the ideas above into a few simple charts that tell you whether propagation is healthy. The first chart shows the share of server spans that have no parent, grouped by service. For services that sit behind others, this number should be close to zero. A sudden jump signals that extract has started failing, often after a deployment or a proxy change.

The second chart shows the average number of services per trace. If this falls after a change, traces are being cut short. The third chart counts incoming requests by header format, with groups such as B3 only, W3C only, both and none. This chart is the key tool during a migration, because it shows when the old format has stopped appearing. Your backend or Collector may need a small rule to record the format as an attribute on a span, and you should add it only if your privacy rules allow it.

i
Good to know

Review these charts after every infrastructure change, not only after application changes. Gateways, meshes and load balancers affect propagation just as much as your own code.

15Advantages, Disadvantages and Trade-offs

B3 is neither good nor bad on its own. Whether it suits you depends on what already runs in your system.

Advantages

  • Simple to understand and easy to read by eye in logs.
  • Supported by a very large ecosystem of Zipkin-era tools, tracers and proxies.
  • Offers a deferred sampling state and a clear debug switch.
  • OpenTelemetry reads and writes it, so old and new services can share one trace.
  • Two forms let you choose between compact and compatible.
  • Years of production use mean most pitfalls are well known.

Disadvantages

  • Not the OpenTelemetry default, so it needs extra packages and settings.
  • No standard place for vendor information.
  • Allows 64-bit trace ids, which clash more easily than 128-bit ones.
  • Maintained by a community practice rather than an official standards body.
  • Running two formats during migration adds work and risk.
  • Some cloud and vendor services understand W3C but not B3.

Trade-offs you will face

ChoiceBenefitCost
Stay on B3 onlyNo change for the existing estateNew tools and partners may not understand it
Write W3C and B3 togetherWorks with both generationsSlightly larger headers and two formats to test
Use the single headerCompact and easy to forwardNot understood by the oldest tools
Use multiple headersMaximum compatibilityMore headers to copy and protect
Trust incoming samplingConsistent traces across servicesExposure to forced sampling from outsiders
Reset sampling at the edgeControl of cost and safetyTraces from external partners start fresh
Analogy

An old but sturdy bridge still carries heavy traffic every day. Rebuilding it takes time and money, and for years it stays in service while a new bridge is built beside it. The wise engineer does not demolish the old bridge until the new one carries the load. B3 is that old bridge: perfectly fine for now, and worth keeping until its successor is ready.

i
Rule of thumb

Keep B3 as long as something you depend on needs it, add W3C beside it for the future, and retire B3 on a plan, not by accident.

Questions to ask before you decide

Choosing a format becomes simple when you ask a few plain questions. Who calls me? If any caller sends only B3, I must be able to read B3. Whom do I call? If any callee reads only B3, I must write B3. Who sits in between? Gateways and meshes in the middle may need their own settings. Who is outside my company? Partners and cloud services usually prefer the W3C standard. How long will the old services live? If they will retire soon, a short overlap is enough; if they will live for years, treat B3 as a long-term guest.

The answers tell you whether you need a bridge, how long it must stay, and which team owns the retirement plan. Write the answers down in a short document, because the question “why is B3 still on?” tends to come back months later.

Example: a quick decision

A team builds a new service that is called by an old Java service and calls a managed cloud database. The caller sends B3, and the database does not take part in tracing. The team therefore enables W3C and B3 for reading, writes W3C and B3 for neighbours that may need either, and records in its notes that B3 can be removed after the old caller is retired.

16Patterns, Anti-patterns and Best Practices

Teams that run propagation well repeat a few habits. Teams that struggle usually share a few mistakes.

Patterns that work

Pattern

One list, set centrally

Define the propagator list once, in shared configuration, so every service starts with the same setting.

Pattern

Bridge services

Place dual-format services at the border between generations and keep the rest of each side simple.

Pattern

Edge reset

Gateways at the public edge decide sampling themselves and ignore outside values.

Pattern

Synthetic cross-service check

A scheduled request that crosses many services verifies each day that the trace stays connected.

Pattern

Header allow-lists

Gateways, proxies and browsers explicitly allow the tracing headers to pass.

Pattern

Documented estate map

Keep a simple table showing which service reads and writes which format.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
What it looks like

A new OpenTelemetry service is deployed with the default W3C setting into an estate that sends only B3.

Why it hurts

Every call from an older service starts a fresh trace, and nobody notices because nothing fails.

Do this instead

Check what the neighbours send before deploying, and enable the matching reader.

ANTI-PATTERN 02Avoid
What it looks like

A public gateway forwards the sampling and debug values it receives from internet callers straight into the system.

Why it hurts

Outsiders can force full recording of their traffic, raising cost and exposing the tracing backend to floods.

Do this instead

Reset or ignore untrusted sampling values at the edge.

ANTI-PATTERN 03Avoid
What it looks like

A team writes its own header parser by hand instead of using the propagator package.

Why it hurts

Small errors with id lengths, letter case or the order of fields cause silent trace breaks, and fixes must be copied everywhere.

Do this instead

Use the maintained propagator and let it handle edge cases.

Best practices at a glance

1

Read generously, write strictly

Accept every format you might receive, and send only what your neighbours need.

2

Prefer 128-bit trace ids

They make collisions practically impossible and match W3C.

3

Make one decision on sampling

Decide at the start and honour that decision downstream.

4

Forward headers in pass-through components

Gateways, caches and routers must copy tracing headers along.

5

Test across the border

After every change, send a request through the full chain and view the single trace.

6

Plan retirement

Give B3 an end date, and track the readers that still need it.

Common mistakes

!
Mistake

Changing the propagator list in only half of the services. One side writes a format that the other side does not read, and the trace splits exactly there.

!
Mistake

Forgetting the browser allowed-headers list, so traces never begin in the front end even though the back end is perfectly set up.

!
Mistake

Writing uppercase letters in hexadecimal ids, or comparing header names with case sensitivity, which fails on systems that lowercase everything.

17Real-World Walkthrough and Industry Usage

Let us hunt a broken trace in an imaginary company, then see where the same ideas appear in well-known tools.

ShopFast: the missing half of the trace

ShopFast is an imaginary online store. Its older Java services were built with Brave and send B3 single-header values. A new recommendations service, written in Python with OpenTelemetry, was added last month. On launch day the team notices that every recommendation request shows up as a separate trace with no connection to the shopper’s page request. The recommendations service looks fast and healthy, yet the page is slow, and the traces cannot explain why.

flowchart LR
Web["Web page"] --> GW["API gateway"]
GW --> Old["Orders service: Java, sends B3"]
Old --> Rec["Recommendations service: Python, default W3C"]
Rec --> DB["Catalog database"]
Figure 7: The new service reads W3C only, so the B3 headers from the orders service are ignored.
1

Notice the symptom

Recommendation spans appear as root spans with no parent, while the Java services show complete trees.

2

Check the incoming headers

A debug log of headers on the new service shows a single b3 header on each call, and no traceparent.

3

Compare with the configuration

The propagator list is the default, tracecontext and baggage. B3 is not read, so extract finds nothing and starts a new trace.

4

Apply the fix

The team installs the B3 propagator package and sets the list to include b3 as well. Now extract reads the header and the trace continues.

5

Verify and protect

A test request shows one tree from the page to the database. A scheduled synthetic check is added so the problem cannot return unnoticed.

With the trace joined, the slow step is clear: a catalog query inside the recommendations service takes most of the page’s time. The fix took one afternoon, and the cause was never a bug in OpenTelemetry, only a mismatch of formats between neighbours.

Where the same ideas appear in industry

Zipkin and the Java tracing family

Zipkin, created at Twitter, defined B3, and its Java tracer Brave still uses it by default. Services built with this family typically keep sending B3 until they are moved to another format, which is why bridges are common.

Service meshes and proxies

The Envoy proxy and meshes built on it, such as Istio, have supported Zipkin and B3 style propagation. Many meshes were set up with B3 in earlier years, and newer releases offer OpenTelemetry-based tracing with W3C options. Operators often need both during migration.

Spring applications

Spring Cloud Sleuth long used B3 by default. Its successor, Micrometer Tracing, commonly defaults to W3C, and it can be set to B3 where required. Companies with many Spring services often run both generations side by side.

Observability vendors and clouds

Vendors such as Datadog, Grafana Labs, Honeycomb, New Relic, Splunk and Dynatrace accept OpenTelemetry traces, and OpenTelemetry’s multi-format propagation lets customers keep B3 on legacy links while using the vendor’s tooling. Cloud providers also publish OpenTelemetry distributions that support several formats.

Illustrative story: BookNest moves its mesh

BookNest, another imaginary company, runs about two hundred services behind a service mesh whose proxies write B3. The platform team wants the mesh to use W3C so that new services need no special setup. They first update every OpenTelemetry service to read both formats. Then they switch the mesh to write both formats for a month while counting how many requests still arrive with B3 only. When the count drops to zero, they stop writing B3 in the mesh, remove the B3 package from the services, and update the documentation. At no point did any trace break, because every step kept at least one common language between neighbours.

The format matters less than the agreement. Two services that share one, whichever it is, tell one story.

18Frequently Asked Questions

Short answers to the questions people ask most often.

Q1What does B3 stand for?
It comes from BigBrotherBird, the early name of the Zipkin project at Twitter. Three B’s became B3. Today it simply names the Zipkin-style trace headers.
Q2Is B3 part of OpenTelemetry’s default setup?
No. The default is W3C Trace Context plus baggage. B3 is available as an optional propagator that you add when you need to work with services that use it.
Q3What is the difference between b3 and b3multi?
In the OpenTelemetry propagator list, b3 writes the single-header form and b3multi writes the multiple-header form. Both carry the same facts, and the propagators can read either form.
Q4Which headers does multiple-header B3 use?
X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled and X-B3-Flags. The parent header is optional, and the flags header is used only for debug.
Q5How long is a B3 trace id?
Either 16 or 32 lowercase hexadecimal characters, which are 64 or 128 bits. OpenTelemetry extends a 64-bit id with leading zeros to 128 bits when it reads it.
Q6What does a sampled value of 0 mean?
It means “do not record this trace”. A service that follows the parent decision will skip recording its spans, and it will pass the same decision on to the next service.
Q7What happens if the sampling value is missing?
The decision is deferred. The receiver uses its own sampling rules and then passes its decision downstream. This is different from a denial.
Q8Can a service use both W3C and B3 at once?
Yes. Put both in the propagator list. On sending, the service writes both sets of headers, and on receiving, it reads whichever it finds. This is the standard way to bridge generations.
Q9Why do my traces break at one particular service?
The usual causes are a format mismatch, a proxy or gateway that removes the headers, or a browser that blocks custom headers. Check the headers arriving at the broken service and compare them with its configuration.
Q10Does B3 carry baggage or business data?
No. B3 carries only trace identity and the sampling decision. Business key and value pairs use the separate baggage propagator.
Q11Is B3 secure?
The headers contain no secrets, but they are untrusted input when they come from outside. Validate them, and do not let public callers force sampling or debug states.
Q12Does B3 affect performance?
The effect is very small: writing and reading a short header takes negligible time, and the size is around a hundred bytes. The larger cost comes from how many traces you record, which sampling controls.
Q13Should I migrate away from B3?
In most cases, over time, yes, because W3C is the default and the wider standard. Migrate in steps: read both formats everywhere, write both, measure remaining B3 use, then retire it.
Q14How is propagation different from the semantic conventions?
Propagation makes sure spans belong to the same trace. Semantic conventions make sure each span describes itself with agreed names. You need both for a clear, connected picture.
Q15Where do I find the exact package for my language?
Look in the OpenTelemetry documentation for your language, under propagators. Names and options change between versions, so confirm them against the version you use.
Q16Why does my trace start at the second service?
The first service probably did not write headers that the second one understands, or something in between removed them. Compare the formats on both sides and check proxies and gateways.
Q17Do I need B3 if I only use OpenTelemetry?
Not usually. If every service uses the default W3C format, B3 adds nothing. You need it only when something outside your OpenTelemetry services speaks B3 and you want those traces connected.
Q18What is the difference between propagation and exporting?
Propagation passes identity between running programs through headers. Exporting sends finished spans from a program to a backend or Collector. They happen at different moments and over different paths.
Q19Can the Collector fix B3 problems?
The Collector sits on the export path, not on the request path, so it cannot repair headers in flight. It can help with sampling and with cleaning data, but propagation must be fixed in the services, proxies and gateways that handle the requests.

19Summary and Key Takeaways

If you remember only a handful of ideas from this guide, make it these.

Key Takeaways

  • B3 is a propagation format. It writes the trace id, span id and sampling decision into request headers so the next service can continue the same trace.
  • It has two shapes. The multiple-header form uses X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled and X-B3-Flags. The single-header form packs the same facts into one b3 header.
  • Sampling travels with identity. Accept, deny, debug, and the deferred state let every service follow one decision made at the start.
  • OpenTelemetry does the work. The propagator injects headers when sending and extracts them on arrival, and you choose formats through the OTEL_PROPAGATORS list.
  • B3 is a bridge, not the default. W3C Trace Context is the default; use B3 where existing Zipkin-era services, proxies or libraries still send it, and list both formats in bridge services.
  • Broken traces are silent. Format mismatches, stripped headers and browser rules cause them, so test across borders and monitor for unexpected root spans.
  • Migrate in steps and guard the edge. Read both formats first, then write both, then retire B3, and never let untrusted callers force sampling or debug.