Span Links - Connect Async and Batch OpenTelemetry Spans

SigNoz Cloud - This page applies to SigNoz Cloud editions.
Self-Host - This page applies to self-hosted SigNoz editions.

Span links connect a span to related spans without making them parent and child. Use them to correlate work across asynchronous boundaries, such as a message queue or a scheduled batch job, where the related spans live in separate traces.

A single process-batch span in the orders-batch service links back to multiple receive-order spans, each in its own source request trace, across an async boundary
A single batch span links back to every source request it processed, a many-to-one relationship that crosses the async boundary into separate traces

Overview

A span link is a typed pointer from one span to another. OpenTelemetry uses links to associate one span with one or more spans, implying a causal relationship, even when one span never directly called the other. Each link stores the SpanContext of the target span, its trace ID and span ID, plus zero or more attributes. The target can sit in the same trace or a separate one.

Parent-child works when a caller starts an operation and waits for it inside the same trace. That breaks once work crosses a queue or runs later in a batch: the downstream operation begins a fresh trace with its own trace ID and no parent, so the path back to what triggered it is gone. A span link restores that path.

Add links when you start the span. A head sampler sees only what exists at span creation, so a link attached afterward may not influence its sampling decision.

AspectParent-childSpan link
RelationshipDirect call, caller waitsCausal, no direct call
Trace boundarySame traceSame or different trace
CardinalityOne parent per spanMany links per span
Typical useSynchronous callsQueues, batch jobs, fan-out/fan-in

The OpenTelemetry messaging conventions use span links, rather than parent-child, as the default way to correlate producers and consumers. Three constraints drive that choice:

  • A span has exactly one parent. When one span processes a batch of items that each arrived in a different trace, parent-child can represent at most one of them. Links represent all of them.
  • A consumer often runs inside another active context, such as an HTTP server span. That context is already its parent, so the tie back to the producer has to be a link.
  • Links stay consistent across queue, streaming, and pub/sub systems, whose delivery models otherwise differ.

Common use cases

  • Batch processing and fan-in. A scheduled job processes many independent requests in one run. Each request created its own trace earlier. The batch span links to every request it handles, so you keep traceability from the batch back to each source.
  • Asynchronous messaging. A producer publishes to a queue and a consumer processes the message later in its own trace. The consumer span links to the producer span. Instrumentation libraries for common brokers propagate the trace context for you. For a single message, you can set the producer context as the parent; a link keeps the consumer in its own trace.
  • Fan-out. One span starts several parallel operations. Each downstream span links back to the initiator.

This example uses Python for a batch job that processes work from many separate traces. An API receives requests through the day, traces each one, and enqueues it. Every hour, a job processes the batch in one span that links to each original request.

The same pattern applies in any OpenTelemetry SDK. For the link API in your language, see the manual instrumentation guides for Python and Java.

Capture trace context when you enqueue work

The batch runs after each request finishes, so the worker cannot use the original span object. Capture the request's trace context as W3C Trace Context headers and store them with the queued item:

producer.py
from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

tracer = trace.get_tracer("orders.api")
propagator = TraceContextTextMapPropagator()

def handle_request(item):
    with tracer.start_as_current_span("receive-order"):
        # ... validate and store the incoming request ...

        # Capture the current trace context to persist with the item.
        carrier = {}
        propagator.inject(carrier)        # writes "traceparent" into carrier
        item["trace_context"] = carrier   # store alongside the item (DB or queue)
        enqueue(item)

propagator.inject() writes the active span's context into carrier as a traceparent value. Persist carrier with the item so the batch job can rebuild the context later.

When the job runs, rebuild a link from each stored context and pass them all to one span:

batch_worker.py
from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

tracer = trace.get_tracer("orders.batch")
propagator = TraceContextTextMapPropagator()

def process_batch(items):
    links = []
    for item in items:
        ctx = propagator.extract(item["trace_context"])      # carrier -> Context
        span_context = trace.get_current_span(ctx).get_span_context()
        if span_context.is_valid:
            links.append(trace.Link(span_context))

    with tracer.start_as_current_span("process-batch", links=links) as batch_span:
        batch_span.set_attribute("batch.size", len(items))
        for item in items:
            reconcile(item)
  • propagator.extract() turns each stored carrier back into a Context.
  • trace.get_current_span(ctx).get_span_context() reads the source request's SpanContext from that context.
  • trace.Link(span_context) builds one link per request, and links=links attaches them all to process-batch at creation.

The process-batch span now references every request it handled, a many-to-one relationship that a single parent cannot express.

Run the batch job, then open the Trace Explorer to inspect the traces.

Open the batch trace from orders-batch and select the process-batch span. In the Span Details panel, open the Links tab. The count (Links 5) matches batch.size, and the tab lists one Linked Span ID for every request the batch processed.

Batch trace Span Details panel with the Links tab open, listing a Linked Span ID for each source request
Batch trace: the Links tab lists a Linked Span ID for every request the batch processed

Each Linked Span ID is the receive-order span from a request's own trace. Click one to open that trace, even though it lives separately from the batch run.

A source request trace with the receive-order span and its validate and persist-order children
Source request trace: where a Linked Span ID takes you

You can trace backward from a batch run to each request it handled, which helps debug async and scheduled work.

Limitations

  • The Links tab lists span references that are not parent-child relationships. SigNoz renders parent-child relationships in the waterfall.
  • The Links tab shows only each linked Span ID, not the linked span's attributes or events. Click the badge to open that span for the full detail.

Next steps

Get Help

If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack.

If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.

Last updated: June 08, 2026

Edit on GitHub

Was this page helpful?

Your response helps us improve this page.