How Does Context Propagation Work Across Message Queues in OpenTelemetry?

How Does Context Propagation Work Across Message Queues in OpenTelemetry?

A request that travels through a queue stops being one request in the eyes of your monitoring tools, unless something carries its identity across the gap. This guide explains what that something is, how it rides inside messages, and what design choices decide whether your traces stay whole.

Imagine a relay race where the runners cannot see each other. The first runner finishes a lap and drops the baton into a mailbox. Hours later, a different runner opens the mailbox, picks up the baton, and runs the next lap. A referee with a stopwatch wants to record this as one race. But the baton carries no number, no team name, and no start time. To the referee, it looks like two unrelated sprints.

This is exactly what happens in software when a service puts a message on a queue and another service takes it off later. Without extra help, the monitoring system sees two disconnected pieces of work and cannot tell that one caused the other. The extra help is called context propagation: a small piece of identity data, attached to the message, that lets the second service say “I am continuing the story that the first service began.”

This article teaches the whole subject from zero. You will learn what context is, how it normally travels between services, why a queue makes the job harder, where the identity data hides inside a message, how producers and consumers use it, when to connect work with a parent link and when with a looser link, and how real systems such as Kafka, RabbitMQ, and Amazon SQS handle it. Every new term is explained when it first appears, and every big idea comes with an everyday comparison.

1Why Queues Break the Story

Message queues are everywhere in modern systems, and they are exactly where observability tends to go dark.

What a message queue is

A message queue is a middleman that holds messages until someone is ready to handle them. A service that sends a message is called a producer. A service that receives and handles it is called a consumer. The middleman itself is called a broker. Well-known brokers include Apache Kafka, RabbitMQ, Amazon SQS and SNS, Google Pub/Sub, and Azure Service Bus.

Teams use queues for good reasons. A queue lets the producer finish quickly without waiting for slow work. It absorbs sudden bursts of traffic. It lets consumers fail and recover without losing work. And it lets many separate services react to the same event. An online shop, for example, may place an “order created” message on a queue, and then the payment, stock, email, and analytics services each handle it in their own time.

What a trace needs

A trace is the full story of one request as it passes through many services, and each step of the story is a span. For a trace to join spans from different services, they must share a trace ID, a long random number, and each span must know the ID of its parent span. That identity data is the span context. When two services talk directly over the web, the context travels inside the web request. Queues remove that direct conversation. The producer never talks to the consumer. It only talks to the broker, and the consumer only talks to the broker.

55 bytes
size of the standard trace header,
tiny compared with most messages
0 to hours
typical waiting time of a message
between send and handling
1 to many
number of consumers that may
react to one message
Everyday Analogy

A phone call is synchronous: both people are on the line, so each knows who the other is. A letter in a mailbox is asynchronous: the sender walks away and the reader arrives later. If the sender wants the reader to know that this letter is a reply to an earlier conversation, the reference number must be written on the letter itself, because the mailbox will not remember it. Context propagation is writing that reference number on the message.

What goes wrong without it

If the producer and consumer do not share context, each creates its own separate trace. A slow order in the shop shows up as a fast “create order” trace and a separate slow “handle order” trace, with nothing to connect them. Engineers cannot see that one caused the other. They cannot measure how long the message waited in the queue. They cannot follow a failure back to its source. Dashboards that count end-to-end journeys become impossible to build.

i
Production Example

Amazon built propagation into its own messaging path. When a service that uses X-Ray sends a message to an SQS queue, the trace header is stored in a special system attribute named AWSTraceHeader, and a Lambda function triggered by that queue picks it up automatically. This is why a trace in AWS can span an API call, a queue, and a function without any manual work.

2Core Concepts: The Vocabulary of Propagation

A handful of words carry the whole topic. Learn these and the rest becomes much easier.

Idea

Context

A bundle of values that belongs to the current piece of work, such as the active span and any baggage. Inside a program it is passed along automatically through threads or async calls.

Idea

Span context

The part of context that identifies a span: trace ID, span ID, trace flags (including the sampled flag), and an optional trace state.

Tool

Propagator

A small component that knows how to write context into a message and read it back. Different formats have different propagators.

Action

Inject

Writing the current context into a carrier, such as the headers of an outgoing message.

Action

Extract

Reading context out of a carrier, such as the headers of an incoming message, to build a context object.

Thing

Carrier

The object that physically holds the context while it travels: HTTP headers, message headers, message attributes, or a field in the payload.

Extra

Baggage

Optional key-value pairs, such as a customer tier, that travel alongside the trace identity and can be read by every service downstream.

Link

Span link

A pointer from one span to another span, possibly in a different trace, used when a parent-child relationship is not the right description.

The W3C Trace Context format

The most common format is defined by the W3C, the organization that sets web standards. It uses a header named traceparent, which has four parts separated by dashes: a version, the 32-character trace ID, the 16-character ID of the parent span, and two characters of flags. One flag bit says whether the trace is being sampled, meaning kept for storage. A second header, tracestate, can carry extra vendor-specific values. The total traceparent is always 55 characters long, which is why it adds so little weight to a message.

Other formats

Older and vendor-specific formats still exist: B3 from Zipkin, the Jaeger format, and the Amazon X-Ray format. OpenTelemetry supports them through separate propagators and lets you combine several in a composite propagator, which writes all of them and reads whichever it finds. This is vital during migrations, when half of your services speak one format and half another.

Everyday Analogy

Think of passports. Every country issues its own, but there is an international standard for what a passport must contain, so border officers everywhere can read it. The W3C format is the international passport standard for traces. A composite propagator is an officer who can also read the older national passports from countries that have not switched yet.

Two kinds of travel

Context moves in two different ways, and beginners often confuse them. In-process propagation carries context inside one program, across function calls, threads, and async tasks, using thread-local storage or similar tools provided by the language. Inter-process propagation carries context between separate programs, using inject and extract through a carrier. This article is mostly about the second, but the first matters too, because context lost inside a consumer is just as harmful as context lost on the wire.

3The Baseline: Propagation Over Web Calls

Before adding a queue, look at the simple case. It shows the pattern that every messaging system copies.

The five-step dance

When service A calls service B over HTTP, five things happen. First, A starts a span for the outgoing call. Second, A asks the propagator to inject the current span context into the request headers. Third, the request travels to B. Fourth, B asks the propagator to extract the context from the incoming headers. Fifth, B starts its own span as a child of the extracted context. The trace ID stays the same throughout, and the parent link ties the two spans together.

sequenceDiagram
    participant A as Service A
    participant P as Propagator
    participant B as Service B
    A->>A: Start client span
    A->>P: Inject current context into request headers
    P-->>A: Headers now hold traceparent
    A->>B: HTTP request with traceparent header
    B->>P: Extract context from request headers
    P-->>B: Parent context rebuilt
    B->>B: Start server span as child of extracted context
    B-->>A: HTTP response
Figure 1: Context propagation over a direct web call. The same two steps, inject and extract, will reappear with queues.

Why this works so smoothly

The web request is a natural carrier. It has headers, it is sent immediately, it gets exactly one response, and there is exactly one receiver. Because the call is synchronous, the parent span is still open when the child starts, so the relationship is clear. Instrumentation libraries for web frameworks do all five steps automatically, which is why many teams never think about propagation until the first queue appears.

Everyday Analogy

Handing a baton directly from one runner’s hand to the next works because they are standing side by side. Both runners know the baton exists, the handover is immediate, and the referee sees it happen. Queues remove the side-by-side part. The baton must now survive being left alone for a while.

i
Tip

Make sure a global propagator is actually configured. In some language SDKs, the default propagator does nothing until you set one, and in others the default is the W3C format plus baggage. If nothing is set, every service quietly starts its own trace. Test this first with a plain web call before you debug queues.

Configuring propagators in practice

Propagators are normally chosen once, at application start-up, either in code or through a standard environment setting that lists the formats to use, separated by commas. A typical choice is the W3C trace context together with baggage. During a migration, the list may include a second format, so that both old and new services understand each other. Because the setting is global, a change affects every call the program makes, web and messaging alike. Keep the setting identical across all services in a system, or at least document every difference, since a mismatch is the single most common reason traces stop at a boundary.

4What a Queue Changes

Queues are not just slower web calls. They change the shape of the relationship between sender and receiver, and propagation must adapt.

PropertyDirect web callMessage queue
TimingReceiver handles it right nowReceiver may handle it seconds, hours, or days later
Parent spanStill open while the child runsUsually already finished before the child starts
Number of receiversOneZero, one, or many
Number of senders per receiver actionOneA batch can mix messages from many senders
DeliveryOne attempt, one answerMay be redelivered, duplicated, delayed, or sent to a dead letter queue
CarrierStandard HTTP headersDepends on the broker: headers, properties, attributes, or none at all
Failure feedbackError comes straight backProducer never learns that the consumer failed

Three gaps to bridge

The first gap is a time gap. The producer’s span has ended long before the consumer’s span begins. This raises a design question: should the consumer’s span still be called a child, or only related? Later chapters explore this choice in depth.

The second gap is a cardinality gap. One message may feed many consumers, and one consumer may handle many messages at once. A tree-shaped trace, in which every span has a single parent, cannot describe these many-to-many relationships without extra tools such as links.

The third gap is a carrier gap. Every broker stores metadata differently, and some do not allow any metadata. The inject and extract logic must be adapted to each broker’s way of attaching extra data to a message.

flowchart LR
    subgraph NOPROP["Without propagation"]
        P1["Producer trace A"] --> Q1["Queue"]
        Q1 --> C1["Consumer trace B, unrelated"]
    end
    subgraph WITHPROP["With propagation"]
        P2["Producer span in trace A"] -->|"traceparent in message headers"| Q2["Queue"]
        Q2 -->|"headers delivered with message"| C2["Consumer span in trace A or linked to it"]
    end
Figure 2: Without propagation, the queue splits the story in two. With propagation, the consumer can rejoin it.
Everyday Analogy

A hospital sends a blood sample to a laboratory. The nurse who took the sample and the technician who tests it never meet, and the test happens hours later. For the results to reach the right patient, the sample tube carries a label with the patient’s ID. The label is the context. The lab does not need to know the nurse. It only needs the label to rebuild the story.

!
Common Trap

Do not assume that your web framework’s automatic instrumentation covers queues. It does not. Web instrumentation hooks into HTTP clients and servers. A message client is a different library with its own hooks. Always verify that a messaging instrumentation library exists for your broker and your language, and that it is switched on.

5The Carrier: Where Context Hides in a Message

Every message has a body and some metadata. Context belongs in the metadata, never in the body.

Why not put it in the body

The message body is the business content, such as the order details. Mixing tracing data into it forces every producer and consumer to agree on a changed schema, breaks strict validation, pollutes data that may later be stored for years, and tempts developers to start using the trace ID as a business key. Metadata, by contrast, is separate and can be added or ignored without touching the business content.

Metadata styles by broker

Broker or serviceWhere context is storedThings to watch
Apache KafkaRecord headers (key and byte value pairs)Needs a broker and message format that support headers, which modern versions do
RabbitMQ and AMQPThe headers table in the message propertiesHeader names are case sensitive, so match exactly
Amazon SQSMessage attributes, plus the special trace header system attributeA message may carry only a small, fixed number of custom attributes
Amazon SNSMessage attributesSame small limit, and forwarding to SQS needs attribute delivery enabled
Google Pub/SubMessage attributesLimits on count and size of attributes
Azure Service BusApplication propertiesAzure client libraries use their own property name for the trace identity
JMSMessage propertiesProperty names must follow naming rules
NATSMessage headers in recent versionsOlder versions have no headers
Amazon KinesisNo metadata at allContext must be placed in an envelope around the payload
Redis streams and listsFields inside the entry, or noneAdd dedicated fields for context
MQTT 5 and MQTT 3.1.1User properties in version 5. No metadata in version 3.1.1Older protocol needs an envelope

The getter and setter idea

Because every carrier looks different, OpenTelemetry’s propagators never touch a carrier directly. Instead, they use two tiny helpers. A setter knows how to write one key and value into the carrier. A getter knows how to read a key, and how to list all keys. When you build messaging instrumentation, you write a getter and setter for your broker’s header type, and the standard propagators do the rest. This is why one propagator works across web requests, Kafka headers, and AMQP properties alike.

Everyday Analogy

A court interpreter does not need to know every language’s grammar books by heart. She works with a translator at each side. The setter and getter are those translators: they let the same message about the trace be written in, and read from, many different kinds of envelopes.

!
Common Trap

Header values in some brokers are raw bytes, not text. The getter must decode them using the right character encoding, and the setter must encode them the same way. A mismatch, such as writing text as one encoding and reading it as another, yields garbled trace IDs that silently fail to link. Test a full round trip with unusual characters in other headers to be sure nothing else interferes.

6The Producer Side, Step by Step

The producer has one job when it comes to propagation: make sure the message leaves with the right identity attached.

What the producer does

1

Start a producer span

Before sending, the producer starts a span that represents the act of creating or publishing the message. It is marked with the producer kind, so tools know it hands work off to someone else.

2

Make that span the current one

The new span is set as the active span in the current context. This matters because the propagator will write whichever span is active, and you want it to write this one.

3

Inject context into the message

The propagator writes the trace ID, span ID, and flags into the message’s headers or attributes using the setter for that broker. Baggage may be written too, if you use it.

4

Send the message

The client library transmits the message to the broker. The span records how long the send took and whether the broker accepted it.

5

Record attributes and end the span

The span stores facts such as the broker type, the destination name, and the message ID if the broker returns one, then ends.

sequenceDiagram
    participant PR as Producer Service
    participant BR as Broker
    participant CO as Consumer Service
    PR->>PR: Start producer span, make it current
    PR->>PR: Inject traceparent into message headers
    PR->>BR: Send message with headers
    BR-->>PR: Acknowledge receipt
    PR->>PR: End producer span
    Note over BR: Message waits in the queue for seconds, minutes, or hours
    BR->>CO: Deliver message with the same headers
    CO->>CO: Extract context from headers
    CO->>CO: Start consumer span with extracted context as parent or link
    CO->>CO: Handle the message and end span
    CO->>BR: Acknowledge message
Figure 3: The full life of a message. Inject happens before sending, extract happens after delivery, and the queue holds the identity in between.

A subtle point: which span’s identity is written?

The context written into the message is the one that is active at the moment of injection. If you inject before starting the producer span, the message carries the identity of whatever span was active earlier, perhaps the incoming web request’s span. If you inject after starting the producer span, the consumer will point back to the send operation itself. The second choice is usually better, because it describes the actual handoff, and it is what the semantic conventions (the shared naming and structure rules for telemetry) recommend.

What about messages created now and sent later?

Some systems separate creation from sending. A message may be built in one place, placed in a buffer, and sent in a batch by a background thread. The context must be captured at creation time, not at send time, because by the time the background thread runs, the original request’s context is gone. Good instrumentation writes the context into the message when it is created and treats the send as a separate step.

Everyday Analogy

A mail clerk writes the return address on an envelope at the moment she seals it, not when the postal van picks it up. If she waited until pickup, the van driver, who handles hundreds of envelopes, would write the wrong address on many of them.

i
Tip

Always inject into the message that will actually be sent, not into a copy. Some client libraries build a new immutable message object during sending. If you inject into the old object, the headers are lost. Verify by reading the headers of a message on the broker side during testing.

7The Consumer Side, Step by Step

The consumer must rebuild context from the message and then make sure that context stays alive while the work is done.

1

Receive the message

The client library delivers a message, often as part of a batch. The message still carries its headers or attributes exactly as the producer wrote them.

2

Extract the context

The propagator reads the headers through the getter and builds a context object holding the remote span context. If no valid headers exist, it returns an empty context and the consumer simply starts a new trace.

3

Choose the relationship

Decide whether the consumer span becomes a child of the extracted context, or a new root with a link back to it. The next chapter explains how to choose.

4

Start the consumer span

Start a span of the consumer kind for the processing, set the right attributes, and make it the current span so that everything done during processing becomes its descendant.

5

Do the work

Business code runs inside the span. Calls to databases and other services create child spans automatically, because the context is active.

6

Record the outcome and end

On failure, mark the span as an error and record the exception. Then end the span, and acknowledge or reject the message according to your broker’s rules.

If extraction finds nothing

Not every message carries context. Messages sent before instrumentation was added, messages from third-party producers, and messages from older services will arrive with empty headers. Extraction must never fail loudly in that case. It should return an empty context, and the consumer span starts a new trace. Counting how often this happens, as a metric, tells you how much of your system still lacks propagation.

If extraction finds garbage

Headers can also be malformed, truncated, or malicious. A well-written propagator validates the format and ignores anything invalid, so that a bad header cannot crash the consumer or poison the trace with nonsense identifiers. If you write a custom propagator, validate the lengths and characters of every field.

Keeping context alive while working

Extracting context is only the start. The consumer must make that context active for the duration of the work. If the framework hands the message to a worker thread, or if the handler is asynchronous, the active context may not travel with it. Child spans created inside the handler then attach to the wrong parent or to none. Section 16 of this guide covers in-process propagation in more detail, but remember now that “extract” and “make current” are two separate steps.

Everyday Analogy

A relief nurse starting a shift reads the patient’s chart, which is the extract step. But reading the chart is not enough. She must keep it open at the bedside and write on it throughout the shift, which is the make-current step. If she reads the chart and then does her rounds without it, nothing she does will be recorded in the patient’s file.

8Parent-Child or Link? The Central Design Choice

After extracting context, you must decide how the consumer span relates to the producer span. There are two main answers, and each has a price.

Option one: make the consumer a child

In a parent-child relationship, the consumer span uses the producer’s span as its parent. Both share the same trace ID, so the trace viewer draws one connected tree: the web request, the send, the long wait, the handling, and everything the handler did. This gives the most natural picture of a single business transaction and makes end-to-end timing easy to read.

Option two: start a new trace and add a link

In a link relationship, the consumer starts a new trace with its own trace ID, and the consumer span carries a link pointing to the producer’s span context. The two traces are separate, yet each can find the other. This keeps traces short and independent, while still preserving the connection.

Parent-child: advantages

  • One trace shows the whole business transaction
  • End-to-end latency is visible in a single view
  • Sampling decision is shared, so both sides are kept or dropped together
  • Simple to understand and to search

Parent-child: drawbacks

  • Traces can become huge and last for hours or days
  • Tail sampling may judge the trace before the consumer finishes
  • A message that fans out to many consumers creates a very wide trace
  • Redeliveries and retries clutter the tree with repeated children

Link: advantages

  • Traces stay small, bounded, and quick to open
  • Works naturally for batches, where one span has many sources
  • Each trace gets its own sampling decision and lifetime
  • Good for very long delays between send and handling

Link: drawbacks

  • The story is split, so you must follow a link to see the other half
  • Many backends show links less clearly than parent-child trees
  • Sampling can drop one side while keeping the other
  • Whole-journey timing needs extra queries or metrics
flowchart TD
    S{"How many messages does one consumer span handle?"}
    S -- "Exactly one" --> D{"Is the delay between send and handling short and bounded?"}
    S -- "Many, as a batch" --> L1["Use links from the consumer span to every message context"]
    D -- "Yes, seconds to minutes" --> P1["Make the consumer span a child of the producer context"]
    D -- "No, hours or unbounded" --> L2["Start a new trace and add one link to the producer context"]
    P1 --> F{"Does the message fan out to many consumers?"}
    F -- "No" --> OK["Keep parent-child"]
    F -- "Yes, many" --> L3["Consider links to keep each consumer trace small"]
Figure 4: A rule of thumb for choosing between parent-child and link relationships.

What the conventions say

The OpenTelemetry messaging conventions suggest that for a span handling a single message, the context extracted from the message is used as the parent, and for spans that handle several messages at once, links are the right tool. The conventions leave room for judgment, and many teams adopt a hybrid: parent-child for normal short delays, and links when delays become long or when a message goes to many receivers. Whatever you choose, apply it consistently across services, so that engineers know what to expect when they open a trace.

Everyday Analogy

A family photo album can show a wedding and the honeymoon on the same page (parent-child) or put them on separate pages with a note saying “see page 40” (link). The first is easy to read when the events happened close together. The second is more practical when the honeymoon was a year later and there are many other photos between them.

A worked example of the choice

Consider an online shop. When a customer places an order, the web service publishes an “order placed” message, and the payment service handles it within a second or two. These services form a tight conversation, so the payment span is a child of the publishing span, and the customer’s checkout trace shows the whole journey. The same message also reaches a reporting service that reads it in batches every ten minutes. For that service, a batch span with links is the honest answer, because each batch holds hundreds of orders from hundreds of checkouts. Finally, a reminder service receives a “send review request” message scheduled for three days later. It starts a fresh trace with a single link back to the order, because a trace that stays open for three days would be unusable. One message, three consumers, three different relationships, each chosen to fit the way that consumer works.

9The Messaging Span Model

Propagation tells spans how to connect. The messaging conventions tell them what to look like, so that every tool can read them the same way.

The operations of a message

The conventions describe a message’s life as a handful of operations. Create means building the message. Send (or publish) means handing it to the broker. Receive means asking the broker for messages and getting some back. Process means actually handling a message’s content. Settle means telling the broker the outcome, such as acknowledging success or rejecting it. Instrumentation produces spans for the operations that make sense for the broker in use.

OperationTypical span kindWhat the span covers
CreateProducerBuilding the message. Context is written at this point
Send or publishProducerHanding the message to the broker and waiting for confirmation
ReceiveConsumerFetching messages from the broker, such as one poll call
ProcessConsumerRunning business logic for a message or batch
SettleClientAcknowledging or rejecting the message

Naming and attributes

Span names follow a simple pattern: the operation followed by the destination, such as “send orders” or “process orders”. Using the destination (the queue or topic name) rather than the message content keeps names stable and low in variety, which matters for grouping and for storage. The span also carries attributes, which are labels that describe it.

Attribute

Messaging system

Names the broker family, such as Kafka, RabbitMQ, or Amazon SQS, so tools can group and filter correctly.

Attribute

Destination name

The queue, topic, or exchange the message was sent to or received from.

Attribute

Operation type

Which step this span describes: create, send, receive, process, or settle.

Attribute

Message ID

The identifier assigned by the broker or producer, useful for finding a specific message in logs.

Attribute

Body size and batch count

How large the message was and how many messages a batch span covered.

Attribute

Consumer group and client

Which group of consumers took part, which helps explain how work was divided.

!
Common Trap

The exact attribute names in the messaging conventions have been revised across versions, and older instrumentation libraries may emit older names. When you build dashboards that depend on these names, check which version of the conventions your libraries follow, and expect to handle both the old and new names during transitions.

i
Tip

Never put the message body, customer names, or other business content into span names or attributes unless you have confirmed it is safe. High-variety names create too many distinct groups, and personal data in telemetry creates privacy risk. Use the message ID and destination as handles, and look up details in the system of record.

10Batches: Many Messages, One Span

Real consumers almost never fetch one message at a time. Batches are where simple propagation breaks and links earn their keep.

The batch problem

A Kafka consumer calls a method that returns a batch of records, perhaps five hundred of them, coming from many different producers and many different traces. A span that covers the fetching of that batch has five hundred possible parents. A tree allows only one. Choosing a single message’s context as the parent would be arbitrary and would hide the other four hundred ninety-nine.

The solution: links

The receive span for a batch uses links, one for every message context in the batch. The span itself starts a fresh trace or continues the consumer’s own context. Each link says “this work was triggered by that message”. A trace viewer can then show, for any message’s trace, which batch span picked it up, and for any batch span, every message that fed it.

flowchart LR
    M1["Message 1 from trace T1"] --> R["Receive span for one batch"]
    M2["Message 2 from trace T2"] --> R
    M3["Message 3 from trace T3"] --> R
    R --> PR1["Process span for message 1, link to T1"]
    R --> PR2["Process span for message 2, link to T2"]
    R --> PR3["Process span for message 3, link to T3"]
Figure 5: A batch receive span fans out into per-message process spans. Links keep the connections to each producer trace.

Per-message spans inside a batch

Many teams combine two levels. A single receive span covers the fetch, with links to all messages. Then, for each message, a process span is started with the message’s own context as the parent, or with a single link, depending on the choice from the previous chapter. This gives two benefits: the fetch is timed once, and each message’s handling appears in its own producer trace, which is exactly where an engineer investigating a particular order wants to see it.

Limits of links

Links cost memory and storage. A span with five hundred links is heavy. SDKs also apply limits to the number of links on a span, silently dropping extras. For very large batches, consider linking only a sample of the messages, or only the ones that were sampled, and record the batch size as an attribute so that the count remains visible. The goal is useful navigation, not a perfect index.

Everyday Analogy

A postal sorting facility receives a sack with letters from hundreds of senders. The facility’s log records one entry, “sack opened at 9:05”, plus a list of the letters’ tracking numbers. Each sender can later look up their own letter’s tracking number and see that it went through the sack. The sack entry is the batch span, and the tracking numbers are the links.

11Fan-Out, Fan-In, and Request-Reply

Queues allow communication shapes that web calls cannot. Each shape asks something different of propagation.

Fan-out: one message, many consumers

In a publish-subscribe system, a single message is delivered to several consumers, each in its own group. The producer wrote its context once, and every consumer extracts the same context. If all consumers choose parent-child, they all become children of the same producer span, and the trace grows several branches, one per consumer. That is a true picture of what happened, and it is often exactly what you want for a moderate number of consumers.

The picture can become unwieldy when the number of consumers is large, or when consumers run at wildly different times, so that the trace stays “open” for days as late consumers join. In such cases, links keep each consumer’s trace short and independent.

flowchart TB
    PRD["Producer span: publish order created"] --> BRK["Topic on the broker"]
    BRK --> C1["Consumer group A: payment service"]
    BRK --> C2["Consumer group B: stock service"]
    BRK --> C3["Consumer group C: email service"]
    BRK --> C4["Consumer group D: analytics service"]
    C1 --> S1["Process span, child or link of producer span"]
    C2 --> S2["Process span, child or link of producer span"]
    C3 --> S3["Process span, child or link of producer span"]
    C4 --> S4["Process span, child or link of producer span"]
Figure 6: Fan-out. One producer context is shared by every consumer group, and each decides how to relate to it.

Fan-in: many messages, one result

Fan-in is the reverse. An aggregator may wait for ten messages, such as parts of an order from ten services, and then produce a single combined message. The new message has ten possible ancestors. The honest way to record this is for the aggregator’s span to link to all ten contexts, then inject its own context into the combined message. Downstream consumers then continue from the aggregator’s span, and the ten source traces remain discoverable through links.

Request-reply over queues

Some systems use queues for remote procedure calls. The caller sends a request message with a “reply to” queue name and a correlation identifier, then waits for the answer on the reply queue. Propagation must work in both directions. The request carries the caller’s context. The reply carries the responder’s context. The caller, on receiving the reply, can use the extracted context as the parent of the span that continues its work, or simply keep working inside its original span if it blocked while waiting.

It is worth separating two ideas here. A correlation ID is a business-level identifier that ties a request to its reply in the application’s logic. A trace ID is an observability identifier that ties spans together for monitoring. They often serve different audiences and should not be merged. Using the trace ID as the correlation ID would make your application logic depend on your tracing setup, which can be sampled, reconfigured, or switched off.

Everyday Analogy

A town crier announces news to the whole square (fan-out), and each listener goes off to act in their own way. A committee secretary collects ten reports and writes one summary that lists the sources (fan-in). A customer who leaves a voicemail asking to be called back gives a call-back number and a reference code (request-reply). The reference code is for the business, while the secretary’s filing number is for the office’s own record keeping.

12Apache Kafka in Detail

Kafka is the most common streaming platform, and it has features that shape how propagation behaves.

How Kafka stores messages

Kafka keeps messages, called records, in ordered logs called topics, which are split into partitions. Records are not deleted when they are read. They remain for a configured retention period, and each consumer group tracks its own position, called an offset. A consumer can even go backwards and read old records again. This durable log design has several effects on tracing.

Headers carry the context

Each Kafka record can carry headers: a list of names with byte values. The producer instrumentation injects traceparent and optional tracestate and baggage headers into this list, and the consumer instrumentation reads them from each record after a poll. Headers have been supported by Kafka since version 0.11, so modern deployments handle them. Very old clients or message formats do not.

Kafka-specific behaviors that surprise people

Batching

Polls return batches

One poll returns many records from many producers. This is the classic case for a receive span with links, plus a process span per record.

Replay

Old records, old context

Reprocessing records from last week attaches new work to traces that may have been deleted from your tracing backend. Links then lead nowhere.

Rebalance

Consumers move

When partitions move between consumers, in-flight work may be repeated by another consumer, creating a second process span for the same record.

Streams

Stateful processing

Joins and aggregations in stream processors merge records from many sources. Context follows the fan-in rules, usually via links.

Copy

Mirroring and connectors

Tools that copy records between clusters, or move data to databases, must copy headers too, or the trace ends at the first copy.

Compaction

Keys and tombstones

Special records used to delete state carry little or no payload, but headers can still be useful for tracing the cause of a deletion.

Ordering and partitions

Kafka guarantees order only within a partition. Spans from records in different partitions can run in parallel on different consumers. A trace viewer will show these as concurrent siblings. Do not expect the span order to match the order of production across partitions. Adding the partition number and the offset as attributes helps engineers locate a specific record when debugging.

i
Production Example

The OpenTelemetry Java agent instruments the Kafka client libraries automatically. It injects headers on send, creates receive and process spans on the consumer side, and links batches. For many Java teams, this means a working trace across Kafka with no code change. Teams on other languages often rely on community instrumentation libraries, or inject and extract by hand using the same pattern.

Everyday Analogy

Kafka is like a long roll of ticker tape that readers can rewind. Every reader keeps their own bookmark. A bookmark moved back a week means the reader re-reads old messages, and the tickets stapled to those messages, the headers, still show the original journey, even if the original office closed its records long ago.

Consumer groups and trace shape

A Kafka consumer group is a set of consumers that share the work of reading a topic, so that each partition is read by one member at a time. If two different groups read the same topic, each sees every record, which is the fan-out case. A trace viewer will therefore show one producer span with one branch per group. When a group is scaled from three consumers to thirty, the trace shape does not change, because the branch belongs to the group, not to the individual consumer. What does change is the spread of spans over hosts, so add the client or host identity as an attribute when you want to see which member handled which record.

13RabbitMQ, SQS, SNS, and Lambda

Queue-style brokers behave differently from log-style brokers, and cloud services add their own conventions.

RabbitMQ and AMQP

RabbitMQ receives messages at an exchange, which routes them to one or more queues. Consumers pull messages from queues and send back an acknowledgement when done. Context travels in the message’s headers table. Because messages are removed after acknowledgement, replay is rare, and the delay between send and handling is usually short, which makes parent-child a natural choice. The consumer’s span should cover the handling and end around the time of the acknowledgement. Watch for header name case: AMQP headers are case sensitive, so a producer writing Traceparent and a consumer reading traceparent will not find each other.

Amazon SQS

SQS stores messages in a queue until a consumer deletes them. Consumers poll for batches, and messages that are not deleted become visible again after a timeout, which is how redelivery works. Custom context can ride in message attributes, but SQS limits a message to a small number of them, so writing several tracing headers and also your own business attributes can hit the limit. AWS also provides the AWSTraceHeader system attribute, which carries the X-Ray format header and is populated when an X-Ray-aware sender writes to the queue. Many teams use both: the X-Ray attribute for AWS-native tools, and a W3C attribute for OpenTelemetry-based tools.

Amazon SNS and fan-out to queues

SNS publishes one message to many subscribers, often several SQS queues. Message attributes can travel through SNS to the subscribing queues, but only if the subscription is configured to deliver the original attributes as attributes rather than wrapping the whole message in an envelope. Wrapped delivery hides the attributes inside the body, and the consumer must then unwrap it before extraction. This is a frequent reason why traces stop at SNS.

AWS Lambda as a consumer

When Lambda reads from SQS, the service itself delivers batches of messages to the function. The platform passes the trace header from the message to the function’s environment so that X-Ray traces connect. For OpenTelemetry, the function’s instrumentation must extract the W3C context from the message attributes of each record, and decide between parent-child for single-record handling and links for batches. The function’s short life means spans must be flushed before it freezes, and a missed flush looks like missing context but is actually missing data.

Broker styleTypical delayReplayNatural relationship
Work queue (RabbitMQ, SQS)Short, bounded by consumer speedRare, only through redeliveryParent-child for single messages, links for batches
Log (Kafka, Kinesis)Short to very longCommonLinks for batches and replays, parent-child when live and short
Pub-sub (SNS, Google Pub/Sub)ShortSometimesParent-child with moderate fan-out, links with large fan-out
Scheduled or delayedPlanned, longNoneLinks, because the gap is intentional
!
Common Trap

Teams often add tracing to their services and forget the managed pieces between them: topics, subscriptions, rules, and function triggers. The weakest hop decides whether the whole chain stays connected. Walk through each hop and prove that headers or attributes survive it.

14When the Broker Has No Headers

Some brokers give you nowhere to put metadata. You can still propagate context, but you must build the carrier yourself.

The envelope pattern

If the broker offers only a payload, wrap the payload in a small envelope that holds two sections: a metadata section and a data section. The metadata section carries the trace context fields, and the data section carries the original business content. The producer builds the envelope, and the consumer unwraps it. The pattern works with any broker, including Kinesis, plain Redis lists, files, and older protocol versions.

Design questionRecommendation
Where do the fields go?In a reserved metadata object that is separate from business fields
Which format?Use the same W3C field names, so the standard propagator can read them through a custom getter
How to stay backward compatible?Let consumers accept both enveloped and plain messages, and roll out consumers first
What about schemas?Version the envelope and register it, so strict consumers do not reject new fields
What about size limits?Keep only the traceparent and, if necessary, a short baggage. A message limited in size should not be filled with tracing data

Costs of the envelope

An envelope changes the message format, so every consumer must understand it, including tools that peek into the stream for debugging or analytics. It also tempts people to read tracing fields as business data. Keep the metadata block clearly named, document it, and never rely on it for application logic.

Everyday Analogy

A postal service that allows no stickers on parcels forces you to put the tracking slip inside the box. It works, but now the person who opens the box must know to look for the slip, and anyone who moves the contents to a different box must remember to carry the slip along.

i
Tip

If you have a choice of broker version or protocol version, prefer one that supports headers or properties. Spending a little effort on an upgrade is almost always cheaper than owning a custom envelope in every producer and consumer for years.

15Outbox, Retries, Dead Letters, and Replays

Real systems are full of patterns that move a message through extra stages. Each stage is a chance for context to fall off.

The outbox pattern

Many services need to update a database and publish a message as one reliable action. The outbox pattern solves this by writing the message into a table in the same database transaction as the business change. A separate relay process later reads the table and publishes the messages to the broker. The relay runs long after the original request ended, so it has no active context. If the outbox row does not store the trace context, the published message will have none.

flowchart LR
    A["Web request span"] --> B["Service writes order and outbox row in one transaction"]
    B --> C["Outbox row stores traceparent column"]
    C --> D["Relay or change capture tool reads the row later"]
    D --> E["Relay restores context from the stored traceparent"]
    E --> F["Relay publishes message with headers"]
    F --> G["Consumer extracts and continues the trace"]
Figure 7: The outbox pattern. The trace context must be saved in the outbox row to survive the delay.

The fix is to store the current traceparent value in a column of the outbox row at write time, and have the relay restore it as the parent or link when publishing. Some change-data-capture tools, such as Debezium’s outbox event router, support carrying tracing information through this path. Check the documentation of the tool you use.

Retries and redelivery

When a consumer fails and the broker delivers the message again, the second attempt extracts the same context and creates a second process span. Under parent-child, both attempts are siblings under the producer span. Mark each attempt with a retry count attribute, and record the failure of the first on its span, so the trace explains why there are two. Without that detail, engineers see duplicate work and suspect a bug in the producer.

Dead letter queues

After too many failures, a broker may move a message to a dead letter queue, a holding area for problems. The message and its headers usually travel along. A tool or a person later inspects it and perhaps replays it. Good practice is to preserve the original trace context, and add attributes about why the message was moved and how many times it was tried. A replay should then start a new trace with a link to the original, because the gap may be days, and the original trace may have expired.

Delayed and scheduled messages

Messages meant for the future, such as a reminder in three days, make a planned time gap. Parent-child would keep a trace “open” for three days, which many backends handle poorly. A link is the clean choice: the scheduled consumer starts a new trace and links to the scheduling span.

Everyday Analogy

A hospital discharge note is written during the visit, filed in a tray, and posted to the family doctor days later by an office clerk who never met the patient. If the clerk is not given the patient’s reference number when the note is filed, the doctor receives an anonymous letter. The outbox row’s saved trace context is the reference number written on the note at the moment it is filed.

16Keeping Context Alive Inside the Consumer

Extracting context from a message is only half the work. The other half is making sure it stays attached to the work that follows.

How a program remembers the current context

Inside one program, OpenTelemetry stores the current context in a place that depends on the language. In some languages it is thread-local storage, a variable that has a separate value for each thread. In others it is an object passed explicitly through function calls, or a special variable that follows asynchronous tasks. Instrumentation reads this “current context” whenever it creates a child span or makes an outgoing call.

Where context gets lost

Threads

Thread pools

If the message handler hands work to a thread pool, the worker thread has its own empty context. Work done there is not connected to the consumer span.

Async

Callbacks and futures

Code that continues later on a different thread or event loop may lose the context unless the framework carries it.

Reactive

Reactive pipelines

Reactive frameworks move work across schedulers. They need explicit context bridges or instrumentation that hooks into them.

Batch

In-memory buffers

A service that collects messages in a buffer and processes them on a timer loses the link between each message and its context unless it keeps them together.

Framework

Listener containers

Messaging frameworks call your handler from their own threads. Check that their instrumentation activates the extracted context around your code.

Manual

Hand-written consumers

If you write the poll loop yourself, you must extract, start the span, and activate it, and then end the span even when errors occur.

The fixes

The standard fixes are to wrap thread pools and executors so that they capture the current context when a task is submitted and restore it when the task runs, to use the context-aware versions of async helpers that your language provides, and to keep the message and its extracted context together in the buffer, activating the right one when each item is handled. Most OpenTelemetry SDKs and agents offer these wrappers. Check which ones exist for your stack.

Everyday Analogy

A doctor who hands a patient’s case to a colleague must hand over the chart as well. If the colleague arrives at the bedside without the chart, every test he orders is recorded in a blank file. Wrapping a thread pool is like adding a rule that says “no case moves to another doctor without its chart”.

!
Common Trap

A leaked context is as bad as a lost one. If a worker thread keeps the context of the last message it handled, and does not clear it, the next message’s work may attach to the wrong trace. Make sure the code that activates a context also deactivates it when the work ends, using the scope or try-finally pattern your language supports.

17Sampling Across Queues

Sampling decides which traces are kept. Queues make this tricky, because the two halves of a story may be judged at different times and by different services.

A quick reminder

Sampling means keeping only a fraction of traces to control cost. With head sampling, the decision is made when the trace starts and is carried in the sampled flag of the trace header. With tail sampling, a collector waits until most of the trace has arrived and decides then, based on what happened, such as errors or slowness. Both interact with queues.

Head sampling and parent-child

When the consumer is a child of the producer, the consumer’s parent-based sampler reads the sampled flag from the extracted context and follows it. Both sides are kept or dropped together. This is the cleanest outcome. A message whose producer was dropped will produce a consumer span that is also dropped, saving cost on both sides.

Head sampling and links

When the consumer starts a new trace with a link, its root sampler makes a fresh decision, independent of the producer’s. The result may be that the producer trace is kept and the consumer trace dropped, or the reverse. Engineers then follow a link into a trace that does not exist. A link-aware sampler fixes this by looking at the links when the span starts and choosing to keep the new trace whenever a linked trace was sampled. The sampler interface in OpenTelemetry passes the links to the sampler for this purpose.

flowchart TD
    A["Consumer receives message with traceparent"] --> B{"Relationship chosen?"}
    B -- "Child of producer" --> C["Parent based sampler reads the sampled flag"]
    C --> D["Same decision as producer: kept together or dropped together"]
    B -- "New trace with link" --> E{"Sampler inspects links?"}
    E -- "Yes, link aware" --> F["Keep new trace if the linked trace was sampled"]
    E -- "No, independent root" --> G["Random decision: halves may be mismatched"]
Figure 8: How sampling decisions flow across a queue under each relationship choice.

Tail sampling and long delays

A tail sampling collector holds spans for a fixed decision wait, often tens of seconds. A message that waits in a queue for ten minutes will produce consumer spans long after the producer’s spans were judged. Under parent-child, those late spans arrive for a trace that has already been decided. Some collectors remember recent decisions, but only for a limited time and number of traces, so late spans may be dropped or treated as orphans. This is a strong reason to prefer links when queue delays are long, and to give the consumer side its own tail sampling policies, such as keeping all traces with errors.

CombinationResultAdvice
Child plus head samplingProducer and consumer kept or dropped togetherSimple and consistent. Best for short delays
Link plus independent head samplingHalves may mismatch, leaving dangling linksAvoid, or use a link-aware sampler
Link plus link-aware samplingLinked traces kept together on the consumer sideGood for long delays and batches
Child plus tail sampling, short delayWorks, as long as delay is under the decision waitCheck the wait against queue latency
Child plus tail sampling, long delayConsumer spans arrive too lateSwitch to links, or lengthen the wait with care
i
Tip

Compute metrics about queue behavior, such as how long messages wait and how many fail, from all spans before any sampling happens. Then dashboards stay accurate even when the traces that explain them are sampled, and you avoid depending on a perfectly matched pair of sampled traces to see queue health.

18Baggage Over Queues

Baggage lets you carry your own labels along with the trace. Over queues, it becomes both more useful and more dangerous.

What baggage is for

Baggage is a set of key-value pairs that travels through the same propagation path as the trace identity, in a header named baggage. A service early in the chain can put in a value such as the customer tier, and every service later in the chain can read it and, if it wishes, copy it onto its own spans. This lets you group and filter all spans of a request by a business attribute that only the first service knew.

Why queues change the picture

On a web call, baggage reaches one receiver. With a queue, baggage is written into a message that may be read by many consumers, some of which belong to other teams, or even other companies, and the message may sit in storage for days. Anything you place in baggage is then visible to every consumer and visible in any tool that inspects message headers.

Good uses

  • Small, non-sensitive labels such as region, customer tier, or experiment group
  • Values needed by many downstream services for grouping
  • Short-lived hints that avoid repeated lookups

Bad uses

  • Personal data, such as names, emails, or account numbers
  • Secrets, tokens, or authorization decisions
  • Large values that bloat every message
  • Anything that business logic depends on, since baggage can be dropped or altered

Size and limits

The W3C baggage format sets minimum limits that implementations should support, around eight kilobytes in total, and many systems enforce smaller ones. Baggage that grows without discipline can overflow header limits in a broker, particularly in systems like SQS that allow only a small number of attributes. Keep baggage to a handful of short values, and review it regularly.

Copying baggage onto spans

By default, baggage does not automatically appear as span attributes. A span processor can copy chosen keys onto each new span, so that trace searches can use them. Copy only an approved list of keys. Copying everything would turn an accidental secret in baggage into a permanent attribute in your trace storage.

Everyday Analogy

Baggage is like a luggage tag stuck on a suitcase that will pass through many airports. It is fine for a tag to say “priority customer” or “fragile”. It is not fine for the tag to carry the owner’s passport number, because every handler along the way can read it and nobody can recall the tag once it is attached.

19Security and Trust

Context data is input from another program, and input from another program must never be trusted blindly.

Forged or manipulated context

Anyone who can write to your queue can write any traceparent header. A malicious or buggy producer might set the sampled flag on every message, which forces your consumers to record and pay for every trace. It might copy a real trace ID from another team to pollute their traces with unrelated spans. It might send absurdly long headers to waste memory. For queues that accept messages from outside your own organization, treat extracted context as a hint and not as a command.

Practical defenses

  • Validate format strictly. Ignore headers that are the wrong length or contain illegal characters.
  • Do not obey the sampled flag from untrusted sources. Make your own sampling decision and add a link to the foreign context instead of becoming its child.
  • Limit baggage. Cap the number of entries and bytes you accept, and ignore the rest.
  • Strip at boundaries. When a message leaves your trust zone, such as going to a partner, remove internal baggage and decide whether to share trace identity at all.
  • Do not derive security decisions from tracing data. A trace ID or baggage value must never decide who can see or do anything.

Privacy of message headers

Headers are visible to broker administrators, monitoring tools, and sometimes to many consumers. Placing trace IDs there is normally harmless, because they are random, but combining them with other metadata may allow a person with broker access to reconstruct the path of a request. If your data is sensitive, consider who has read access to the queues and whether trace identities should be shared with them.

Data retention

Because messages in a log-style broker live for days or weeks, the trace context in their headers can outlive the traces in your tracing system, and may outlive your retention rules. That is not usually a problem, since an ID alone reveals nothing. It does mean that replayed messages point to traces that no longer exist, which is a nuisance rather than a risk.

!
Common Trap

Teams often trust internal queues completely, then connect them to a partner integration or a public ingestion endpoint, and forget that those producers can now influence tracing behavior. Whenever a new source is added to a queue, review whether its context should be trusted.

20Measuring Propagation Itself

If propagation silently breaks, traces just look shorter than they should. You need numbers that tell you when this happens.

Metrics worth publishing

Coverage

Messages with valid context

The share of consumed messages whose headers held a valid trace context. A drop usually means a producer was deployed without instrumentation.

Errors

Extraction failures

Count of messages whose context headers were present but invalid or unreadable.

Delay

Queue latency

Time between the producer’s create time and the consumer’s start time. This is the clearest number about queue health.

Shape

Orphan consumer spans

Consumer spans that began a new trace even though the topic is supposed to carry context.

Load

Batch sizes and link counts

How many messages each batch covers, and how many links were dropped by SDK limits.

Flow

Publish and process rates

Messages sent and handled per second by destination, to catch backlogs early.

The messaging conventions also define standard metrics, such as the duration of publish, receive, and process operations and counts of sent and consumed messages. Using the standard names lets ready-made dashboards work with your data. As with attributes, metric names have evolved, so confirm what your libraries emit.

The queue latency number

Queue latency deserves special attention. It is computed by subtracting the time recorded when the message was created from the time the consumer started handling it. It tells you directly whether consumers are keeping up. Because it depends on clocks in two different machines, be aware of clock differences. A few milliseconds of skew is harmless when delays are seconds. It misleads when delays are tiny.

Canary messages

A canary is a synthetic message sent every minute through each important queue, with a known destination and a recognizable name. An alert fires if the consumer’s span with the same trace link does not appear within a set time. Canaries test the complete path, including managed pieces such as topic subscriptions, that your instrumentation cannot see.

Everyday Analogy

A postal service that wants to know whether its network works sends a test letter to itself every day and checks that it arrives, how long it took, and whether the tracking slip survived. A single test letter says more about the whole route than thousands of readings from individual sorting machines.

Reading the numbers together

The metrics are most useful in combination. Rising queue latency with steady coverage means consumers are falling behind, so look at capacity. Falling coverage with steady latency means a producer lost its instrumentation, so look at recent deployments. Rising extraction failures after a broker upgrade suggest that the new version rewrites or truncates headers. A sudden surge in orphan consumer spans on one topic points to a new, uninstrumented producer. Teaching on-call engineers these small patterns turns a pile of charts into a quick diagnosis, and saves the long guesswork that otherwise follows every broken trace.

21Cost and Performance

Propagation is cheap per message, but cheap things multiplied by billions deserve a calculation.

The header weight

The standard trace header is 55 bytes. With the header name and the encoding that brokers add around each header, call it about 70 bytes per message. Consider a platform that sends one million messages per second. The added data is about 70 megabytes per second, or around 6 terabytes per day. For most platforms this is a tiny portion of total traffic, because the payloads themselves are far larger. But if your messages are tiny, such as short status updates of a few dozen bytes, the header can double their size, and the cost is no longer negligible.

~70 MB/s
header overhead at one million
messages per second
~6 TB/day
extra data that overhead
adds across a day
2x
possible size growth for
very small messages

CPU and memory

Injecting and extracting are small operations, but they run on every message. They involve string building, parsing, and validating, so avoid creating new objects needlessly and reuse propagator instances. Spans created for each message cost more than the headers do. On a very hot path, sampling the span creation, or creating spans only for a fraction of messages, can save CPU, while still propagating the header so that downstream services can decide for themselves.

The trade-off between richness and cost

Rich propagation

  • Spans for create, send, receive, process, and settle
  • Links for every message in a batch
  • Baggage copied to spans
  • Complete picture, but heavy at high rates

Lean propagation

  • Only the send and process spans
  • Links limited to a sample of batch messages
  • Minimal baggage
  • Cheaper, with less detail for investigators

A good compromise is to start lean for the busiest destinations, and enable richer detail on lower-volume, high-importance queues such as payments or orders. Revisit the choice as traffic changes.

22Testing Propagation

Propagation bugs are quiet. Nothing crashes. Traces just come out shorter than they should. Only deliberate tests catch them.

TestWhat it provesHow to run it
Round trip testContext written by the producer is read correctly by the consumerSend one message through a real or containerized broker, then compare the consumer span’s parent or link to the producer span’s IDs
Header presence testThe message really carries the headers on the brokerRead the raw message from the broker and look for the trace header and its format
Missing header testConsumers survive messages with no contextSend a plain message and confirm a new trace starts without errors
Malformed header testConsumers ignore garbage safelySend truncated, oversized, and wrongly encoded headers
Batch testLinks point to every message context, up to limitsSend mixed messages from several traces, poll once, and inspect the links
Thread hop testContext survives thread pools and async hand-offs inside the consumerMake the handler call a database from a worker thread and check the span’s parent
Retry and redelivery testRepeated attempts appear correctly in the traceForce a failure on the first attempt and check the two spans and their attributes
Cross-broker hop testBridges, connectors, and topic-to-queue forwards keep headersFollow one trace across every managed hop, such as SNS to SQS
Sampling testSampled and unsampled traces behave as designed on both sidesSend messages with the flag on and off, and see which spans exist
Canary in productionThe live path stays connected over timeSend a synthetic message every minute and alert on a missing consumer span

Use an in-memory exporter

Most SDKs include an exporter that keeps finished spans in memory, which lets automated tests read them back and check parents, links, kinds, and attributes. Combine it with a containerized broker for realistic integration tests, and you can run the whole pipeline on a developer’s laptop or in a build server.

Test the sad paths

Write tests where the broker rejects the message, the headers are removed by a proxy, the consumer crashes mid-processing, and a message is replayed a day later. Each of these exercises an error path that real systems will meet, and each tends to reveal a place where context is lost or an exception escapes.

Everyday Analogy

Testing propagation is like testing a parcel tracking system. It is not enough to see that the label prints. You must send a real parcel through every depot, including the strange ones, and confirm that the label is still readable at the end. A single depot that peels off the label breaks tracking for every parcel that passes through.

23Failure Scenarios and Recovery

These are the situations that most often break connected traces across queues. Knowing them in advance shortens every investigation.

Scenario 1: Traces end at the broker

Producer spans exist, consumer spans exist, but they never connect. The usual causes are a producer without messaging instrumentation, a consumer extracting from a differently named header, a managed hop that strips attributes, or a propagator that was never configured. Recovery: look at the raw message on the broker. If the header is absent, the problem is on the producer side or in a hop. If it is present, the problem is in the consumer’s extraction or configuration.

Scenario 2: Two formats, no overlap

The producer writes the W3C format while the consumer reads only the older B3 format, or the reverse. Neither complains. Recovery: use a composite propagator that writes and reads every format in use during the migration, then retire the old format once all services have moved.

Scenario 3: Huge traces from long delays

A scheduled task parented to its creator makes a trace that stays open for days. Trace viewers struggle, tail sampling policies misjudge, and storage costs rise. Recovery: switch long-delay paths to links, set a maximum acceptable delay for parent-child, and make that rule visible in the team’s guidelines.

Scenario 4: Missing context after a retry or dead letter hop

A message moved to a dead letter queue arrives without headers because the relay copies only the payload. Recovery: make sure every component that moves messages copies headers, and add a test that follows a message through the dead letter path.

Scenario 5: Wrong parent after a thread hop

Database spans appear in separate traces rather than under the consumer span, because the handler’s work moved to another thread. Recovery: wrap executors, use context-aware helpers, and add a thread hop test.

Scenario 6: Sampling mismatch

Engineers follow links from a kept producer trace and find nothing, because the consumer trace was dropped. Recovery: use link-aware sampling on the consumer side, or choose parent-child so the decision is shared.

Scenario 7: Header size limit exceeded

Baggage grows, a broker rejects messages with too many attributes, and the producer starts failing. Recovery: cap baggage at the producer, and treat tracing headers as optional so that failure to attach them never blocks the send.

Scenario 8: Clock skew distorts queue latency

Producer and consumer clocks disagree, so queue latency appears negative or inflated. Recovery: keep clocks synchronized, treat very small or negative values as measurement noise, and when precise timing matters, measure waiting time using broker-provided timestamps.

!
Common Trap

Never make message delivery depend on tracing. If the code that injects headers throws an error, catch it, record it, and send the message anyway. Observability exists to support the business, and a monitoring problem must never become a lost order.

24Design Patterns and Anti-patterns

Teams that have connected traces across many queues tend to land on the same good habits, and to regret the same shortcuts.

Patterns that work

Pattern

Inject at message creation

Capture context when the message is built, so later buffering or background sending cannot change it.

Pattern

One company-wide rule

Write down when to use parent-child and when to use links, and apply it everywhere so traces look familiar.

Pattern

Links for batches and long gaps

Use links whenever one span handles many messages, or the delay is long or unbounded.

Pattern

Composite propagation during migration

Write and read every format in use until the old ones can be retired safely.

Pattern

Save context in outbox rows

Store the trace header in the outbox record so the relay can restore it later.

Pattern

Measure coverage

Publish the share of messages with valid context and alert when it drops.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
What it looks like

Trace IDs are written into the message body, or used as business correlation keys.

Why it hurts

Consumers become dependent on tracing. The message schema fills with observability fields. If tracing changes or is disabled, business logic breaks.

Do this instead

Keep context in headers or properties, and use a separate business identifier for correlation.

ANTI-PATTERN 02Avoid
What it looks like

Every consumer span is made a child of its producer, regardless of delay or batch size.

Why it hurts

Traces stretch over days, batches arbitrarily choose one parent, and tail sampling misjudges traces that stay open.

Do this instead

Choose between child and link using a written rule based on delay, batch size, and fan-out.

ANTI-PATTERN 03Avoid
What it looks like

Messages from external partners are trusted as parents, and their sampled flag and baggage are accepted without checks.

Why it hurts

An outsider can inflate your telemetry costs, pollute traces, or inject misleading labels.

Do this instead

Validate headers, ignore foreign sampling flags, cap baggage, and link rather than parent.

ANTI-PATTERN 04Avoid
What it looks like

Failure to inject or extract context raises an error that stops the message from being sent or handled.

Why it hurts

A tracing bug becomes a business outage. Messages are lost or stuck for a reason unrelated to their content.

Do this instead

Catch and record tracing failures, and always continue with the business operation.

ANTI-PATTERN 05Avoid
What it looks like

Instrumentation is switched on and assumed to work, with no tests or coverage metrics.

Why it hurts

A single deployment can silently remove propagation from one service, and nobody notices until an incident needs the trace.

Do this instead

Test round trips in the build, publish coverage metrics, and send canary messages.

25Real-World Examples

Different platforms solved the same problem in slightly different ways. Their choices reveal the trade-offs in practice.

AWS: SQS, SNS, and Lambda with the trace header attribute

Amazon carries the X-Ray trace header through SQS in a system attribute, and Lambda reads it when a queue triggers a function. This makes AWS-native traces connect across services with no code. Teams that use OpenTelemetry on top typically add the W3C header as a message attribute as well, and handle batches in the function with links, since a single Lambda invocation may receive many messages.

Azure Service Bus and Event Hubs: a diagnostic identifier property

The Azure client libraries store the trace identity in an application property named for diagnostics, in a format compatible with the W3C header. Consumers using the same libraries rebuild the context automatically, and the libraries create links for batches. It is an example of a platform adopting a standard format inside its own property naming.

Google Pub/Sub: tracing built into client libraries

Google’s Pub/Sub client libraries support OpenTelemetry tracing, and carry the trace context in a reserved message attribute, so publishers and subscribers using recent library versions connect automatically. It shows how a managed service can hide the carrier details behind the client.

Kafka with the OpenTelemetry Java agent

A Java service using Kafka clients gets send, receive, and process spans, header injection, and extraction without code changes when the agent is attached. Many teams adopt this route first, then use the same conventions by hand in languages without automatic support.

Debezium and the outbox event router

Change data capture tools read database changes and publish them to Kafka. Debezium’s outbox event router can carry tracing information stored in the outbox row into the published message. It addresses the hardest propagation gap: the one between a database transaction and a message that is sent much later by a different process.

Task queues in application frameworks

Job systems built into frameworks, for example Celery in Python, have OpenTelemetry instrumentation that carries context through task headers. They show how propagation fits into higher-level abstractions, where developers think in terms of “tasks” rather than “messages”, yet the same inject-and-extract mechanism sits underneath.

26A Phased Rollout Plan

Connecting traces across every queue in a large organization takes time. A phased plan reduces risk and shows progress early.

1

Inventory every queue and hop

List brokers, topics, queues, bridges, connectors, and managed triggers. Note each one’s header support and who owns it.

2

Set the global rules

Choose the propagation format, the child-or-link rule, and the attribute naming standard. Publish them in a short guide.

3

Fix one critical flow end to end

Pick a business-critical path, such as order placement, and make its trace connected across every hop, using automatic instrumentation where possible.

4

Add tests and coverage metrics

Create round trip tests, add the coverage and queue latency metrics, and begin a canary message for each important destination.

5

Handle the awkward patterns

Address outbox relays, dead letter paths, delayed messages, and header-less brokers, using the techniques described above.

6

Align sampling

Check how head and tail sampling behave across the queue, and add link-aware sampling or longer decision waits where needed.

7

Roll out to remaining services

Expand by team, watching the coverage metric, and retire older propagation formats when all services speak the new one.

8

Review regularly

Quarterly, check for new queues, new producers, and changes in delay patterns, and update the rules where needed.

27Frequently Asked Questions

Q1What exactly travels inside a message for tracing?
Usually a short trace header, with the trace ID, the producer span’s ID, and flags including the sampled flag. Optionally a trace state header and a baggage header travel too. Together they are tiny compared with most messages.
Q2Should the consumer span be a child or a link?
For one message with a short delay, a child is natural. For batches, very long delays, or large fan-out, a link keeps traces small and honest. Pick a written rule and apply it everywhere.
Q3Why do my producer and consumer traces never connect?
Check, in order: whether a propagator is configured, whether the producer injects headers, whether the headers are on the raw message at the broker, and whether the consumer extracts the same header names and format. A managed hop that strips attributes is another frequent cause.
Q4Does Kafka support trace context?
Yes. Kafka records carry headers, and OpenTelemetry instrumentation injects and extracts the trace header through them. Very old client or message format versions without headers cannot do this.
Q5What if my broker has no headers?
Wrap the payload in an envelope with a metadata section for the trace fields, or upgrade to a protocol version that supports headers. Keep the envelope documented and versioned.
Q6How do I trace a batch of messages?
Use a receive span with links to each message’s context, and per-message process spans that use the message’s own context as parent or link. For huge batches, limit links and record the batch size as an attribute.
Q7What happens to tracing when a message is retried?
Each attempt extracts the same context and creates its own process span. Add a retry count and record the failure on the first span so the trace explains the repetition.
Q8How does the outbox pattern affect propagation?
The relay publishes later, outside the original request, so it has no active context. Store the trace header in the outbox row when writing it, and restore it during publishing.
Q9Is baggage safe to use across queues?
Only for small, non-sensitive values. Baggage is visible to every consumer and anyone who can read message headers. Never put personal data or secrets in it, and cap its size.
Q10Can a malicious producer harm my tracing?
Yes, by forging the sampled flag, copying someone else’s trace ID, or sending oversized headers. Validate header format, ignore foreign sampling decisions, cap baggage, and link rather than parent for untrusted sources.
Q11How does sampling work across a queue?
With parent-child, the consumer follows the producer’s sampled flag, so both sides are kept or dropped together. With links, the consumer decides on its own unless you use a link-aware sampler that keeps traces whose linked trace was sampled.
Q12Will tracing slow down my message throughput?
The header adds a small number of bytes and a small amount of CPU per message. Creating many spans per message costs more than the header. On very hot paths, use lean instrumentation or sample span creation, while still propagating the header.
Q13How do I know propagation is working in production?
Watch the percentage of consumed messages with valid context, the count of extraction failures, and queue latency. Add canary messages that must produce a consumer span within a set time.
Q14Do I need to write code, or does auto-instrumentation handle it?
For popular brokers and languages, auto-instrumentation often handles injection, extraction, and span creation. For unusual brokers, hand-written consumers, thread pools, outbox relays, and header-less protocols, you will add some code. Test to find out which applies to you.

28Summary and Key Takeaways

A message queue separates a producer and a consumer in time and in knowledge. Context propagation repairs the gap by writing a small identity record into the message’s metadata when it is created, and reading it back when the message is handled. Everything else is a set of decisions about how to use that record well: whether the consumer becomes a child or a linked new trace, how batches and fan-out are described, how sampling stays consistent, how untrusted senders are handled, and how you will notice when the chain breaks.

Key Takeaways

  • Inject on the way out, extract on the way in: the producer writes the trace header into message metadata, never the body, and the consumer reads it and makes it the active context while it works.
  • Choose the relationship deliberately: use parent-child for single messages with short delays, and links for batches, long or planned delays, and wide fan-out. Write the rule down and use it everywhere.
  • Know your carrier: every broker stores metadata differently, from Kafka headers to SQS attributes. Brokers without metadata need an envelope, and managed hops such as SNS subscriptions can silently strip context.
  • Handle the awkward paths: store trace context in outbox rows, keep it through retries and dead letter queues, and keep it alive across thread pools inside the consumer.
  • Align sampling with the relationship: parent-child shares decisions automatically, while links need link-aware sampling, and long queue delays can defeat tail sampling windows.
  • Treat context as untrusted input: validate headers, ignore foreign sampling flags, cap baggage, and never put personal data or secrets in baggage or span names.
  • Measure and test propagation itself: track context coverage, extraction failures, and queue latency, run round trip tests, and send canary messages so that a silent break is found before an incident needs the trace.