Span context that survives queues and retries, without rewriting every service.

A trace that stops at the edge of a message queue is a trace that abandons you exactly where the interesting problems live. Async boundaries are where context goes to die — unless you carry it across deliberately.

Context is just metadata you forgot to pass

Trace context is a small bag of ids: the trace id, the parent span id, and some flags. Across a synchronous call your framework passes it for you. Across a queue, nobody does — so you have to inject it into the message and extract it on the other side.

Inject on send, extract on receive

// producer
msg.headers["traceparent"] = current_span.context()
// consumer
ctx = extract(msg.headers["traceparent"])
span = start_span("handle", parent=ctx)

Mind the retries

Retries and fan-out complicate the story: one message may be processed several times, or split into many. Model those as links rather than a single parent chain, so a redelivered message shows up as related to the original instead of pretending to be it.

Verify end to end

The only way to know your context survives is to look. Trace a request that crosses a queue and confirm the spans on both sides share a trace id. Do it before an incident, not during one.