What Is a Span Link in OpenTelemetry?

What Is a Span Link in OpenTelemetry?

A plain-English guide to the small pointer that connects one span to another without making either one the parent. You will learn what a link holds, when to use it instead of a parent, how it behaves in batching, messaging, retries, and trust boundaries, and how to keep it from quietly breaking.

Think about a book with footnotes. The main text runs from the first page to the last, like a story with a clear beginning and end. Now and then, a small note says “see also page 212” or “compare with the earlier chapter”. The note does not make page 212 part of this paragraph. It simply says that the two are related and tells you where to look.

Tracing has the same need. A trace is the story of one request, built from spans, which are records of timed steps. Spans normally form a tree: each span has one parent, and the tree shows how the work was nested. But not all work fits into a tree. A job that processes a hundred messages from a hundred different requests has a hundred causes, not one. A request that arrives from an untrusted caller may deserve its own fresh trace, while still remembering who called. For these cases, OpenTelemetry offers the span link.

A span link is a footnote between spans. It says “this span is related to that span” and tells you exactly which one, without changing who the parent is. This guide assumes you know nothing. Every new word is explained the first time it appears, always with an everyday comparison.

1The Short Answer: One Page Before the Long Story

If you only have two minutes, this chapter gives you the whole idea. The remaining chapters explain why each part exists and how to use it well.

A span link is a reference from one span to another span, held inside the first span. The reference consists of the identifiers that name the target span, which are its trace ID and span ID, plus a few optional attributes that describe the relationship. A span can hold zero, one, or many links.

The key difference from a parent is that a link does not form the tree. A parent says “I am part of this span’s work, nested inside it”. A link says “I am related to that span, but not nested in it”. The linked span may even live in a different trace, which a parent never can.

0 or more
links a span may carry, while it has at most one parent
2 IDs
trace ID and span ID, the address of the linked span
Any trace
a link can point across traces, which a parent cannot
QuestionParentLink
How many per span?At most oneZero, one, or many
Same trace required?YesNo, it can cross traces
Shapes the tree view?Yes, it defines the treeNo, it adds a pointer beside the tree
Means nesting in time?Usually, the child happens inside the parentNo timing relationship is implied
Typical useOrdinary call chainsBatches, fan-in, async work, trust boundaries
Everyday Analogy

Think of an email. A reply belongs to one thread, and it has exactly one message it answers, which is like a parent. A “forwarded from” note at the bottom, or a “this was also discussed in that other thread” line, points to something related without making the two threads one. That second kind of pointer is a link.

i
Tip

Remember the shorthand: a parent says “I am inside you”. A link says “I am connected to you”.

2Why Links Exist: When a Tree Is Not Enough

Before learning the details, it helps to feel the problem. A tree works beautifully until one situation appears, and then it breaks down.

A normal trace is a tree. A request comes in and creates a root span. The root calls other steps, which create child spans, which may call further steps. Each span has exactly one parent, except the root, which has none. This shape is natural for calls that wait for answers, and it makes it easy to see where time went.

The situation that breaks the tree

Imagine a service that reads messages from a queue, collects a hundred of them, and processes them together in one go. Each of the hundred messages was sent by a different request, each with its own trace. The processing job has not one cause but a hundred. Which of the hundred traces should the job’s span become part of? Choosing one would be unfair to the other ninety-nine. Choosing none loses the connection altogether.

Other situations create the same puzzle. A request arrives from an outside company with a trace identifier that you cannot trust. You want your own fresh trace, yet you want to remember the caller’s trace. A task fails and is retried an hour later, and you want the new attempt to point back at the old one. A scheduled job runs because of an earlier event, and you want to see which one. A cache computes one result for many waiting requests, and you want the computation to show who it served.

The elegant answer

OpenTelemetry does not bend the tree to fit these cases. It keeps the tree simple, with one parent per span and one trace per tree, and adds a separate, lightweight mechanism for everything else: the link. The link carries the identifiers of any number of other spans, in any traces, and leaves the tree alone.

Everyday Analogy

A family tree shows who is whose parent, and each person has exactly two. But a person may also have friends, mentors, and rivals. You cannot draw those on the family tree without ruining it, so you keep a separate list of connections. Links are that list.

i
Good to Know

An older tracing standard, OpenTracing, had a similar idea called a “follows from” reference. OpenTelemetry replaced the idea with the more general and simpler link.

3Core Vocabulary You Need

A short list of words appears in every conversation about span links. Learn these once and everything else reads easily.

TermPlain meaningEveryday picture
SpanA record of one timed stepOne line on a receipt
TraceAll spans that belong to one request treeOne whole receipt
Trace IDThe long number that names a traceA receipt number
Span IDThe number that names a span inside a traceA line number on the receipt
Parent spanThe span that directly caused and contains this oneA boss in an org chart
Span contextThe small bundle that identifies a span: trace ID, span ID, flags, stateAn address label
LinkA reference from one span to another span’s contextA “see also” note
Linked spanThe span a link points toThe page being referred to
Link attributesKey-value notes describing the linkA note saying why you should look there
PropagationCarrying span context between servicesA label that travels with a parcel
Fan-inMany sources feeding one stepMany streams joining a river
Fan-outOne source feeding many stepsA river splitting into canals

Two ideas hiding in the table

First, a link points to a span context, not to a whole span. The span context is just the identifying bundle. This matters because a link can be created from nothing more than the headers that arrived with a message, without having the other span in memory.

Second, a link is held by the linking span, and it points to the linked span. The linked span does not know it is being pointed to. Nothing is added to it. This one-way nature has consequences for how tools find links, which later chapters explain.

Everyday Analogy

A footnote in a book refers to another book, but the other book does not change because of it. If you want to know who cites a book, you must search through all the other books. The same is true for links, which are stored on the citing side only.

i
Tip

Whenever you read about links, ask which span holds the link and which span is pointed at. Many mistakes come from mixing up the two directions.

4Anatomy of a Link

A link is small and simple. Looking at its parts makes it clear what it can and cannot do.

flowchart TB
    SPAN["Linking span"] --> LINKS["Links list: zero or more links"]
    LINKS --> LINK["One link"]
    LINK --> CTX["Span context of the linked span"]
    LINK --> LATTR["Link attributes: optional key-value notes"]
    CTX --> TID["Trace ID"]
    CTX --> SID["Span ID"]
    CTX --> FLG["Trace flags such as sampled"]
    CTX --> TST["Trace state: vendor-specific values"]
Figure 1: A span holds a list of links, and each link holds a span context and optional attributes

Reading the picture

The linking span holds a list of links, which may be empty. Each link has two parts. The first part is the span context of the span being pointed to. It contains the trace ID, which names the whole trace, and the span ID, which names the exact span in it. Together these two numbers form an address, like a street and a house number. The context also carries trace flags, such as whether that trace was marked for recording, and a trace state, which is a small bag of values that vendors may use.

The second part is a set of link attributes. These are optional key-value notes that describe the link itself, not the spans. They can say why the link exists, for example that the linked span is a previous attempt, or that it is one member of a batch.

What a link does not contain

A link holds no timing information about the relationship, no copy of the linked span’s data, and no promise that the linked span even exists in your backend. It is an address, and the address might lead to a span that was dropped by sampling, or that has not arrived yet. This is a feature, since it keeps links tiny, and also a limit to remember.

Everyday Analogy

A link is like a library call number written on a slip of paper, with a short note on it: “see this for the earlier version”. The slip does not contain the book. If the library no longer owns the book, the slip still reads fine, and the trip to the shelf finds nothing.

i
Good to Know

The span context in a link is the same kind of object that is carried in headers between services, such as the traceparent header of the W3C Trace Context standard. That is why links and propagation work together so naturally.

5Link Versus Parent: Two Different Relationships

The most important idea in this guide is the difference between these two relationships. Almost every design decision comes down to choosing between them.

flowchart LR
    subgraph T1["Trace A"]
      A1["Span A1"] --> A2["Span A2 child of A1"]
    end
    subgraph T2["Trace B"]
      B1["Span B1 root"] --> B2["Span B2 child of B1"]
    end
    B1 -. "link" .-> A2
Figure 2: Solid arrows are parent-child nesting inside a trace. The dotted arrow is a link that crosses traces

What the picture shows

Trace A and Trace B are two separate trees. Inside each tree, solid arrows show parent-child relationships. The dotted arrow from span B1 to span A2 is a link. It tells you that B1 is related to A2, perhaps because A2 caused B1 to happen later, but B1 is not nested inside A2. B1 is the root of its own trace. If you opened Trace B alone, you would see B1 at the top with no parent, and you would find the link in B1’s details, pointing at the other trace.

Why you cannot just use a parent

A parent relationship carries a promise about time and structure: the child ran as part of the parent’s work and typically finished within it. Duration charts and critical path analysis in trace viewers rely on this promise. If you used a parent for a relationship that is not nesting, the viewer would draw a bar for B1 inside A2’s time range, when B1 actually ran an hour later. The picture would be false. A link makes no such promise, so it makes no such error.

RelationshipMeaningConsequence in viewers
Parent and childThis work happened inside that workDrawn as nested bars, used for timing analysis
LinkThis work is related to that workShown as a clickable reference, not drawn as nesting
Everyday Analogy

A chapter inside a book is nested inside it, and its page numbers fall within the book’s range. A citation to a different book does not put that other book inside this one. If a library catalog treated citations as nesting, it would claim your novel contains an encyclopedia.

!
Watch Out

Using a parent for work that is merely related produces misleading timing charts and giant traces. When in doubt about nesting, ask whether the child truly ran inside the parent’s time. If not, a link is probably the right tool.

6What a Link Carries: Context and Attributes

The two parts of a link each have their own job. Understanding both helps you design useful links.

The span context part

The span context identifies the target. The trace ID is a 128-bit number, usually written as thirty-two hexadecimal characters. The span ID is a 64-bit number, written as sixteen. Together, they let a tool find the exact span in a backend, if it was stored. The trace flags say whether the linked trace was sampled, which hints whether it is likely to exist in storage. The trace state is an optional list of vendor values and is rarely touched by ordinary users.

The attributes part

Link attributes are key-value pairs, just like span attributes, but they describe the connection. The OpenTelemetry project does not define a long list of standard link attributes. Teams usually create a small set of their own, such as an attribute that names the kind of relationship. Examples of useful notes include “retry of earlier attempt”, “member of batch”, “triggered by schedule”, and “caller from external system”. The text should be short and stable, with a small set of values, so that tools can group and filter by it.

Context

Trace ID

Names the trace that holds the linked span.

Context

Span ID

Names the exact span inside that trace.

Context

Trace flags

Shows whether the linked trace was marked as sampled.

Attributes

Relationship notes

Optional key-value notes that explain why the link exists.

Keep link attributes simple

Link attributes count against limits and add size to every span that carries a link. If a batch has hundreds of links, a long note on each one multiplies quickly. Choose short, stable values, and avoid putting large or sensitive data there.

Everyday Analogy

A reference slip in a library has the call number, which is the address, and a few words in the margin, such as “earlier edition”. The call number is essential, and the margin note helps the reader decide whether it is worth walking to the shelf.

i
Tip

Decide on one attribute name for the kind of relationship, and a short fixed list of values, before using links widely. Consistent notes make links searchable.

7The Direction of a Link

A link goes one way. This simple fact shapes how you create links, how you read them, and how you search for them.

The span that holds the link is the source, and the span it points to is the target. The target span is created without knowing that anyone will link to it. When a message producer sends a message, it does not know how many consumers will later link back to its span. The link is created later, by the span that wants the reference, using the identifying information that traveled to it.

sequenceDiagram
    participant P as Producer service
    participant Q as Message queue
    participant C as Consumer service
    P->>P: Create send span, span context S1
    P->>Q: Publish message with span context in headers
    Q->>C: Deliver message after some time
    C->>C: Read span context from headers
    C->>C: Create processing span with a link to S1
    Note over C: The producer span S1 is never changed
Figure 3: The consumer creates a link back to the producer’s span context, and the producer’s span is never changed

Reading the sequence

The producer creates a span for sending a message, and that span has a span context. The producer puts the context into the message headers, using the same propagation method used for ordinary calls. The queue holds the message for however long it takes, perhaps seconds or hours. When the consumer receives it, the consumer reads the context from the headers and creates its own span for processing the message. Into that new span, it adds a link carrying the producer’s span context. The producer’s span is already finished and untouched.

Consequences of the one-way design

  • Finding forward links is easy. Open the consumer’s span, and its links are right there.
  • Finding backward links is harder. To learn which spans link to the producer’s span, a tool must search all spans for links that mention it. Some backends index links for this, and others do not.
  • Cheap for the target. Millions of spans can link to one popular span without changing it or slowing it.
Everyday Analogy

If you write a letter that cites an old newspaper article, the newspaper is not notified. Anyone reading your letter can find the article. But if someone wants to find all letters that cite the article, they need a special index of citations.

i
Good to Know

Different tracing backends treat the reverse search differently. Check whether yours can search for spans by the links they hold before you design analysis around it.

8Creating Links: At Start Time Is Best

You can attach links when you start a span, and in newer versions of the API also afterward. The first way is better, and the reason is worth knowing.

Links at span creation

When code starts a span, it can pass a list of links as part of the starting options, together with the name, the kind, the parent, and initial attributes. The span is born already holding its links. This is the recommended way, because everything the span needs for its first decisions is in place from the beginning.

Why the timing matters

The sampler, the component that decides whether to record a trace, runs at the moment a span starts. The sampler is given the links as part of its inputs. A custom sampler might use them, for example to keep any trace that links to a sampled trace. If you add links after the span has started, the sampler has already decided, and it never saw them. Adding links at creation lets the sampler and any early processing use them.

Links added later

Recent versions of the OpenTelemetry API have introduced a way to add a link to a span after it starts. This is useful when you learn about a related span only partway through the work, for example when a batch grows as messages arrive. Support depends on your language and version, and the sampler will not have seen these late links, so use them only when creating them at the start is impossible.

When addedSeen by sampler?Best used for
At span startYesMost cases, since the related spans are known up front
After span startNoCases where related spans become known during the work
After span endNot possible, the span is read-onlyNothing, so decide before ending
Everyday Analogy

Writing the bibliography before submitting a paper lets the editor judge the paper properly. Adding citations after the editor has accepted or rejected it changes nothing about the decision. Links added at creation are in time to influence decisions.

i
Tip

Gather the list of related span contexts before starting the span whenever possible. For message batches, extract the contexts from all messages first, then start the processing span with all of them.

9Use Case: Batch Processing and Fan-In

The classic reason for links is a step that gathers work from many sources. Here is the idea in full.

Suppose a service called “Receipts” reads from a queue where each message represents one order. To save effort, it reads fifty messages at a time and generates fifty receipts in a single batch step. Each message was published by a different request with a different trace.

flowchart LR
    T1["Trace 1: order span"] --> M1["Message 1"]
    T2["Trace 2: order span"] --> M2["Message 2"]
    T3["Trace 3: order span"] --> M3["Message 3"]
    M1 --> Q["Queue"]
    M2 --> Q
    M3 --> Q
    Q --> BS["Batch processing span in a new trace"]
    BS -. "link" .-> T1
    BS -. "link" .-> T2
    BS -. "link" .-> T3
Figure 4: One batch span links back to the spans of the many requests whose messages it handles

What you would do without links

Without links, you have only bad choices. You could make the batch span the child of just one of the fifty messages, which hides the other forty-nine. You could make fifty copies of the batch span, one per trace, which multiplies cost and lies about how many batches ran. Or you could start a fresh trace with no connection, which loses the information about where the work came from.

What links give you

With links, the batch gets its own span, in its own trace, with fifty links, one per message. An engineer investigating a slow receipt for one order can go from that order’s trace to the batch that handled it, by looking for the batch span that links to it. An engineer looking at the batch can see all fifty orders it served, and can jump to any of them. The tree stays honest, and the connections are preserved.

The same pattern in other shapes

The pattern is called fan-in, meaning many sources join into one step. Other examples include a nightly report that aggregates many events, a database write that combines many small updates into one, and a stream processor that handles a window of events together.

Everyday Analogy

A mail carrier collects letters from many mailboxes and drives one delivery route. The route is one trip, with one record. Each letter’s tracking history should say “carried on route 12”, and the route’s record should list the letters it carried. Neither is a sub-part of the other, but both are linked.

!
Watch Out

There is a limit to how many links a span can hold. A batch of five hundred messages can exceed the default limit, and the excess links are dropped. Chapter 19 explains how to handle this.

10Use Case: Messaging Between Producers and Consumers

Message queues are the most common home of links. Yet even here there are two ways to connect the sending and receiving sides, and the choice depends on the shape of the work.

When one service sends a message and another processes it later, the two sides are separated by time. The queue may hold the message for milliseconds or for days. The question is whether the processing span should be a child of the sending span, putting both in one trace, or a new trace that links back to it.

Option one: one message, one trace, parent-child

For a simple flow where each message is handled by exactly one consumer step, many teams and instrumentation libraries make the processing span a child of the producer’s span. The whole journey, from request to send to processing, appears as one trace. This is easy to read. The cost is that a trace can become very long in time, and the waiting time in the queue appears as a gap between parent and child.

Option two: a new trace with a link

When processing is batched, or when the work should be seen as its own unit, the consumer starts its own trace and adds a link to the producer. This keeps traces short and bounded, and it supports many-to-one relationships. The messaging semantic conventions describe both approaches and recommend links in cases where one span relates to many messages, such as when a consumer receives a batch. Details of the conventions have been evolving, so check the current version for the exact rules.

SituationUsual choiceReason
One message handled by one step, short delayParent-child, one traceSimple, the whole story in one place
Batch of many messages handled togetherNew span with links to each messageA span cannot have many parents
Messages that can wait for hours or daysNew trace with a linkAvoids very long, hard-to-read traces
One message fans out to many consumersEach consumer’s span links or is a child, by policyDepends on how you want to view each branch
Everyday Analogy

If you post a postcard and your friend reads it the next day, you might keep both events in one diary page. If the friend collects postcards from fifty people in one sitting and writes one reply for all, the reply needs its own page that mentions each sender. The first is parent-child. The second is links.

i
Tip

Check what your messaging instrumentation does by default before changing it. Libraries differ, and mixing both approaches inside one system creates confusing traces.

11Use Case: Trust Boundaries and Fresh Traces

Sometimes you do not want to continue a trace that arrived from outside. A link lets you start fresh while remembering where the request came from.

The problem with trusting incoming context

When your service receives a request from another system, the request may carry a trace context in its headers. By default, your service continues that trace: its first span becomes a child of the caller’s span, and both share the same trace ID and the same sampling decision. This is perfect inside your own company, where all services cooperate. It is risky at the border with outsiders.

An outside caller could send a trace ID that collides with an existing one, could mark every request as sampled to inflate your volume, or could simply use a different convention for IDs. If you continue their trace, their choices spill into your system. Your trace storage, your sampling, and even your dashboards are influenced by someone you do not control.

The pattern: start new, link to the old

At the public edge, many teams start a new trace for each incoming request, ignoring the caller’s trace as a parent. To avoid losing the connection, they add a link from the new root span to the caller’s span context. Engineers can still follow the connection in either direction, but your trace follows your own rules for IDs and sampling. This pattern is mentioned in the OpenTelemetry guidance for instrumentation at service boundaries, and it is the reason the API allows creating a root span with links.

sequenceDiagram
    participant X as External caller
    participant G as Your gateway
    participant S as Internal services
    X->>G: Request with foreign trace context
    G->>G: Start a new root span in a new trace
    G->>G: Add a link to the foreign span context
    G->>S: Continue with the new trace context
    Note over G,S: Internal sampling and IDs follow your rules
Figure 5: The gateway starts its own trace and keeps a link to the foreign context

Reading the sequence

The external caller sends a request that carries its own trace context. The gateway creates a brand new root span with a new trace ID that the gateway chose. It adds a link that holds the caller’s span context, so the relationship is recorded. Internal services then continue the new trace, which means they follow your sampling decision and your ID format. The foreign context is remembered as a link, and not trusted as a parent.

What you gain

  • Control of sampling. Outsiders cannot force you to record everything by setting a flag.
  • Clean identifiers. Your trace IDs come from your own generator, which protects ratio-based sampling from non-random IDs.
  • A preserved connection. If the partner shares their trace ID in a support case, you can search for links that mention it.
Everyday Analogy

A company’s front desk gives every visitor a fresh internal visitor number, instead of using whatever number the visitor made up. The visitor’s own reference, such as a booking code, is written next to it. Inside the building, everyone uses the internal number, and the booking code is there for lookups.

i
Good to Know

Whether you want this depends on how much you trust the caller. Between your own services, continuing the trace is the right default. At the public edge, a fresh trace with a link is often safer.

12Use Case: Retries and Redelivery

When work fails and is tried again later, you often want each attempt to tell its own story while pointing at the earlier ones.

A quick retry inside one call, such as three tries within one second, is usually shown as sibling spans under one parent. The parent covers the whole operation, and each attempt is a child. This is natural and needs no links.

The picture changes when the retry happens much later. A message that fails processing is put back on the queue and delivered again an hour later. A payment that failed is retried by a scheduled job the next morning. A webhook that was rejected is sent again tomorrow. In these cases, the second attempt happens in a different time and often a different trace. Making it a child of the first would stretch the trace over hours or days. Starting a completely fresh trace would lose the connection.

flowchart LR
    A1["Attempt 1 span in trace X, status Error"] --> Q["Retry queue, waits one hour"]
    Q --> A2["Attempt 2 span in trace Y, status Error"]
    A2 -. "link: previous attempt" .-> A1
    A2 --> Q2["Retry queue, waits one day"]
    Q2 --> A3["Attempt 3 span in trace Z, status Unset"]
    A3 -. "link: previous attempt" .-> A2
Figure 6: Each late attempt lives in its own trace and links to the attempt before it

What the chain shows

Each attempt has its own trace, with its own timing and its own status. The first two attempts ended in Error, and the third succeeded. Each later attempt carries a link to the one before, with an attribute noting that the reason for the link is “previous attempt”. An engineer who finds the failed third attempt, or the successful one, can follow the chain backward to see how many tries it took and why the earlier ones failed.

Why this beats the alternatives

Nesting all attempts under one parent would create a trace that lasts a day and a half and holds spans from unrelated times. Tools would show a huge gap, and the trace would be hard to read and hard to store. Separate unlinked traces would make the chain invisible. Links keep each trace small, and still let you walk the chain.

Everyday Analogy

A customer support ticket that was reopened several times has one record per contact, each saying “follow-up to the earlier contact”. You do not merge them into one long call, and you do not lose the history. The reference is the link.

i
Tip

Store the span context of the failed attempt together with the retry record, for example in the message headers or in the retry table, so the next attempt can create the link.

13Use Case: Scheduled and Triggered Work

Work that starts because of something earlier, but not as a direct call, is a natural fit for a link.

Scheduled jobs

A cron job runs every night with no incoming request. It starts its own trace, because there is no parent. But if the schedule was created by an earlier request, for example a user who asked for a monthly report, it can be helpful to link the nightly job’s span to the span of the request that created the schedule. This explains why the job exists.

Event-driven reactions

In event-driven systems, one service publishes an event, and several others react at different times. A reaction can be a child of the event publication, a link to it, or both, depending on design. Links work well when the reaction happens long after the event, or when the reaction considers many events together.

Webhooks and callbacks

A service asks an outside provider to do something, such as process a payment, and the provider calls back later with the result. The callback arrives as a new request, with no trace context, or with a context the provider chose. A link from the callback span to the original request span, found by looking up a stored span context for that transaction, ties the two together.

TriggerWhy a plain parent does not fitWhat the link points to
Scheduled jobNo request is running when it startsThe request that created the schedule
Event reactionReaction may come much later, or from many eventsThe span that published the event
Webhook callbackThe caller is an outside system with its own contextThe span that made the original request
User action after a notificationThe click happens a day after the email was sentThe span that sent the notification
Everyday Analogy

A reminder alarm goes off at nine every morning, without anyone asking at that moment. Its note says “set up on the third of March because of the dentist appointment”. The note explains the alarm without making the alarm part of the day the appointment was booked.

i
Good to Know

For these cases, you must store the span context somewhere durable, such as a database row or a message, because the later work needs it. Without a stored context, there is nothing to link to.

14Use Case: Splitting Very Long Traces

Some business processes last for hours or days. Keeping them all in one trace creates a monster. Links let you cut the monster into readable pieces.

Consider an online order. The customer places it, it waits for payment confirmation, then for stock allocation, then for shipping, and finally for delivery. The whole life of the order may take a week. If every step were a child in one trace, the trace would be open for a week, the root span would last a week, and the trace would contain thousands of spans over several services. Many backends hold traces in memory for a limited time while waiting for late spans, and such a trace would be cut, incomplete, or very costly.

Cutting by phase

A better design starts a new trace for each phase, and links each phase to the previous one. Phase one, “place order”, is a normal short trace. When payment is confirmed, a new trace begins for “fulfill order”, with a link to the order placement span. When shipping starts, another trace begins with a link to the fulfillment span. Each trace is small, bounded, and easy to read. The chain of links preserves the story.

1

Place order

A short trace that ends when the order is accepted. Its span context is stored with the order record.

2

Confirm payment

A new trace starts when the payment event arrives, and it links to the stored context of the order span.

3

Fulfill and ship

Another trace starts for fulfillment, and it links to the payment span.

4

Deliver

A last trace records delivery and links to the shipping span.

The cost of the design

Cutting a story into pieces means that nobody sees the entire week in one picture. To get the whole story, a tool or a person must follow the chain of links. Some backends can do this, others show only the links as references. Decide whether the loss of the single picture is worth the gain in readable, bounded traces. Most teams find that it is, for processes lasting longer than a few minutes.

Everyday Analogy

A long television series is divided into episodes, each with its own length and story, and each starting with “previously on”. The episodes are separate, easy to watch, and connected. A single twelve-hour film would be hard to store and to watch.

i
Tip

Use a business identifier, such as an order number, as an attribute on every phase’s root span. Even without link-following tools, you can search by that attribute and find all phases.

15Use Case: Shared Work and Coalescing

When one piece of work serves many requests at once, the requests all wait for it, and none of them owns it. Links tell the truth about this sharing.

Imagine a cache that protects a slow database. When a popular item expires, a hundred requests arrive within a second, all wanting it. A smart cache lets only one of them fetch the value from the database, and makes the other ninety-nine wait for that single result. This pattern is called request coalescing or sometimes single flight.

The tracing question

Which request’s trace should contain the database call? Giving it to the first request makes that request look slow for reasons that also affect the others. The other ninety-nine requests show a mysterious wait with no cause. Duplicating the database span into all hundred traces would multiply the apparent database load by a hundred.

The link solution

The single database fetch gets its own span. In the trace of the request that triggered the fetch, it can be a child. In each of the waiting requests, a short “waiting for shared fetch” span carries a link to the fetch span. An engineer looking at a slow request sees “waiting for shared fetch”, clicks the link, and finds the database call and its real duration. One true span exists, and many requests point to it.

Shared work

Cache refresh

One fetch serves many waiting requests.

Shared work

Write batching

Many small writes combined into one database call.

Shared work

Connection setup

One new connection serves a queue of waiting callers.

Shared work

Model loading

One load of a large model or file serves the requests that arrive while it loads.

Everyday Analogy

Many diners at a restaurant wait for the same slow oven to finish a batch of bread. Each diner’s receipt says “waiting for the bread batch”, and the kitchen’s log has one entry for the batch itself. Nobody’s receipt contains the baking time, and everyone’s receipt points to the batch entry.

i
Good to Know

This pattern is advanced and not built into most automatic instrumentation. You usually add it by hand around your own cache or batching code.

16Deciding: Parent, Link, or Just an Attribute?

With three ways to relate things, a short decision path keeps your choices consistent.

flowchart TB
    START["Two pieces of work are related"] --> Q1{"Did the second run inside the first and wait for it?"}
    Q1 -- "yes" --> PARENT["Use parent-child in the same trace"]
    Q1 -- "no" --> Q2{"Does the second span relate to more than one span?"}
    Q2 -- "yes" --> LINK["Use links, one per related span"]
    Q2 -- "no" --> Q3{"Is the gap long, or the trust low, or the trace getting huge?"}
    Q3 -- "yes" --> LINK
    Q3 -- "no" --> Q4{"Is a business ID enough to find the relation?"}
    Q4 -- "yes" --> ATTR["Use a shared attribute such as order ID"]
    Q4 -- "no" --> LINK
Figure 7: A short path for choosing between a parent, a link, and a shared attribute

Explaining the path

First ask whether the second piece of work ran inside the first and waited for it, which is the definition of a call. If so, use a parent. If not, ask whether the second relates to more than one span. A span can have only one parent, so many relations require links. If it relates to one span only, consider the practical issues: a long gap between the two, an untrusted source, or a trace growing unmanageable. Those favor links. If none of these applies, a simple shared attribute, such as an order ID on both spans, may already be enough and costs less.

Why attributes sometimes win

A shared business identifier is the simplest connection. Every span for order 8472 carries the attribute order.id = 8472. A search finds them all, across traces, with no special link support in any tool. Links are better when you need the exact span-to-span relationship, such as which batch handled which message, or when the relationship has no natural business identifier.

ToolStrengthWeakness
Parent-childRich viewer support, timing analysisOne parent only, same trace only
LinkMany targets, across traces, exact span referenceWeaker viewer support, one-way, limited count
Shared attributeSimple, searchable everywhereCoarse, no exact span, relies on a good ID
Everyday Analogy

To connect two documents, you can staple them together, write a reference to a specific page, or put the same case number on both. Stapling is closest, and only fits two documents at a time. A page reference is precise. A case number is simple and works for any number of documents.

i
Tip

Often the best answer is both: a link for the exact reference and a shared business ID for easy searching.

17Getting the Context There: Propagation for Links

A link needs the span context of the target. That context has to travel from where it was created to where the link is made.

Inside one program, getting a span context is easy: you hold the span object, and you can ask it for its context. Across services, the context must be carried with the data. In messaging, this happens by placing the context into the message headers or properties, using the same propagator that carries trace context in ordinary HTTP calls. In the standard format, the context travels as a traceparent header that holds a version, the trace ID, the span ID, and the flags.

flowchart LR
    SPAN["Producer span"] --> INJ["Propagator injects context into message headers"]
    INJ --> MSG["Message with traceparent header"]
    MSG --> Q["Queue or store"]
    Q --> EXT["Consumer extracts context from headers"]
    EXT --> LNK["New span is created with a link to the extracted context"]
Figure 8: The context is injected on the sending side and extracted on the receiving side, where it becomes a link

Inject and extract

The two verbs to remember are inject and extract. The producer injects the current span context into the carrier, which is the message headers. The consumer extracts the context from the carrier and gets back a context object. From it, the consumer reads the span context and uses it for either a parent or a link. Most messaging instrumentation libraries do both steps for you, and also let you choose between parent and link for the consumer side.

When there is no carrier

Not every system has headers. A scheduled job that needs a link to the request that created it must store the span context itself, for example as two text fields in a database table: the trace ID and the span ID, perhaps with the flags. Later, the job reads these fields and builds a span context for the link. Some SDKs allow constructing a span context directly from stored identifiers for this purpose.

What if the context is missing or invalid?

If a message arrives without a context, because the producer was not instrumented or the broker stripped headers, there is nothing to link to. A well-behaved consumer starts a new trace without a link. A link built from an invalid context, with all zeros for the IDs, is meaningless and is typically ignored by the SDK or the backend. Handle this case calmly, and perhaps count how often it happens.

Everyday Analogy

A package’s return address must be written on the package by the sender, and read by the receiver, for a reply to find its way back. If the sender forgot to write it, the receiver cannot reply correctly, and the best they can do is start a fresh letter.

!
Watch Out

Some message brokers, proxies, or bridges between systems drop or rename headers. If your links are missing after a message passes through such a component, check whether the context headers survive the trip.

18Describing the Relationship: Link Attributes

A link by itself says “related”. Link attributes say how, and that single word makes the difference between a mystery and a helpful clue.

Why you need them

A span that carries many links, such as a batch, or a span that carries links of different kinds, such as one retry link and several batch links, needs a way to tell them apart. Without a note, a reader sees a list of identifiers and has to guess what each means. A short attribute removes the guess.

A small, consistent vocabulary

There is no big official list of link attribute names, so teams invent their own. Keep the vocabulary tiny and consistent across services. A single attribute, with a handful of possible values, is usually enough.

Relationship value (example)MeaningTypical span
batch_memberThe linked span produced one of the items in this batchA batch processing span
previous_attemptThe linked span was an earlier try of the same workA retry span
caused_byThe linked span triggered this work indirectlyA scheduled or event-driven span
external_callerThe linked span belongs to an outside system that called usA gateway root span
shared_workThe linked span is the shared computation we waited forA waiting span in a coalescing cache

Good habits for link attributes

  • Keep values few and fixed. A small fixed set allows grouping and counting.
  • Keep them short. Each link carries them, and a batch may have hundreds of links.
  • Keep them private. No personal data or secrets in link attributes.
  • Document them. Put the vocabulary in the team’s instrumentation guide.
Everyday Analogy

In a research paper, citations are often marked “for background”, “contradicts”, or “builds on”. Without those words, a reader sees only a list of titles. A few consistent words turn the list into a story.

i
Tip

Write the names and allowed values of link attributes in the same place as your span naming rules. Treat them as part of the contract between teams.

19Limits: How Many Links Can a Span Hold?

Links are small, but a span with thousands of them would be a problem. The SDK enforces limits, and ignoring them leads to silent loss.

The OpenTelemetry specification defines span limits, which cap how much a span may carry. Among them are a limit on the number of links per span, and a limit on the number of attributes per link. The common default for the link count is 128, and for attributes per link it is also 128, though SDKs may differ, so check yours. When a span tries to hold more links than the limit, the extra ones are dropped, and the span keeps a count of how many were dropped, which is exported with the span as a dropped links count.

A worked example with made-up numbers

A batch consumer reads 500 messages at a time and creates one span with a link for each message. With a limit of 128, only the first 128 links are kept, and 372 are dropped. The span shows a dropped links count of 372. An engineer investigating one of the dropped messages will never find the batch through its link, and may conclude that the batch never happened.

flowchart TB
    BATCH["Batch of 500 messages"] --> Q{"Is the batch larger than the link limit?"}
    Q -- "no" --> OK["Add one link per message"]
    Q -- "yes" --> CH{"Choose a strategy"}
    CH --> S1["Raise the link limit carefully"]
    CH --> S2["Split into several smaller batch spans"]
    CH --> S3["Link only a sample or the most important messages"]
    CH --> S4["Use a shared batch ID attribute on message and batch spans"]
Figure 9: When a batch is bigger than the link limit, choose one of several strategies on purpose

Choosing a strategy

Raising the limit works but costs memory and size for every span, and some backends cap the size of a span or message. Splitting the batch into smaller processing spans, each within the limit, keeps every link and keeps spans small, at the price of more spans. Linking only a subset is acceptable when you need only examples, but it means that some messages cannot be traced forward. A shared batch identifier as an attribute on both the message spans and the batch span is a cheap fallback that works with any tool.

Where to set the limits

SDKs allow setting span limits in code or through environment variables, such as OTEL_SPAN_LINK_COUNT_LIMIT for the link count, and another for attributes per link. Names and defaults can differ by language and version, so confirm in the documentation.

Everyday Analogy

A form has room for ten references. If you have fifty, the extra forty simply fall off the end of the form, and the reader sees ten. You can attach more pages, use several forms, or give a catalog number covering all of them. What you must not do is assume that all fifty were read.

!
Watch Out

Dropped links are silent in most dashboards. Watch the dropped links count on spans, as the monitoring chapter describes, so that you notice when batches outgrow the limit.

20Links and Sampling

Sampling decides which traces are kept. Since links cross traces, sampling can leave a link pointing at nothing.

Each trace is sampled or dropped as a whole. A link from a span in trace B to a span in trace A is just an address. If trace A was dropped by sampling, the address leads nowhere. The trace flags inside the link tell you whether trace A was marked sampled, which is a hint about whether it exists in storage.

Four cases

Linking traceLinked traceWhat you see
KeptKeptA working link, you can jump across
KeptDroppedA dead link, the address leads nowhere
DroppedKeptNo visible link, since the linking span was not stored
DroppedDroppedNothing stored at all

The sampler sees links at start

As discussed earlier, a sampler receives the links of a span when it starts. A custom sampler can use this to keep what matters. For example, a rule might say “if any linked span was sampled, sample this span too”. This raises the chance that both sides of a link are kept, and avoids dead links. Built-in samplers such as the ratio-based one ignore links, so by default each trace is sampled independently.

Tail sampling and links

Tail sampling, which decides after a trace is complete, looks at one trace at a time. It does not follow links to keep the linked traces. If you want to keep a failing batch and the traces of all its messages, you need custom logic, because each message’s trace was a different trace and might already be gone.

flowchart LR
    TB["Trace B: batch span with link"] -. "link" .-> TA["Trace A: producer span"]
    TB --> SB{"Was B sampled?"}
    TA --> SA{"Was A sampled?"}
    SB -- "yes" --> LIVE["Link is visible in B"]
    SA -- "yes" --> TARGET["Target exists and can be opened"]
    SA -- "no" --> DEAD["Dead link: target not stored"]
Figure 10: A link is only useful if the linked trace was also kept

Practical advice

Accept that some links will be dead, and design the experience around it. Show the sampled flag from the link in your tooling so that people do not click on links that cannot work. Where the relationship matters a lot, such as for failed batches, use a custom sampler or a higher sampling rate on the paths that use links.

Everyday Analogy

A footnote that cites a document the library discarded last year still reads fine, and the trip to the shelf finds an empty space. A careful editor marks which cited documents were archived, so readers know which references are worth chasing.

i
Tip

If your links are important, make sure the traces on both ends are sampled together. A simple way is to give link-heavy paths a higher sampling ratio.

21How Backends and Viewers Show Links

A link is only as useful as the tool that displays it. Support varies, so it is wise to know what to expect.

What most viewers do

Most modern trace viewers show the links of a span in its detail panel. Typically, you see a list of the linked spans’ trace IDs and span IDs, and a button or a hyperlink that opens the linked trace if it exists in the backend. Link attributes appear beside each entry. A few viewers draw a small marker on spans that hold links, so you notice them without opening every span.

What many viewers do not do

  • They do not draw links as part of the tree. A link does not change the nesting, so the waterfall remains a normal tree.
  • They may not show reverse links. Showing “spans that link to this one” needs an index over links, which not every backend maintains.
  • They may not follow chains automatically. You may need to click through each hop to follow a chain of retries.
  • They may show dead links without a warning. A click may lead to a “trace not found” page.
CapabilityTypical supportWhat to do if missing
Show a span’s own linksCommonRead raw span data
Open the linked traceCommon when storedSearch by trace ID manually
Search spans by link targetVaries, newer backends add thisAdd a shared attribute such as batch ID
Draw link edges in service mapsRareUse metrics and attributes for flow analysis

Test before you rely

Before designing your tracing around links, create a few example spans with links and look at how your own viewer presents them. If it is clumsy, compensate with shared attributes and documentation that tells people how to find related traces. Do not assume that the experience you imagine is the experience your team will get.

Everyday Analogy

A digital book may mark footnotes as clickable links, or it may print them in small type at the bottom of the page. The content is the same, and the reader’s effort is very different. Check what your reader does before promising anyone a smooth experience.

i
Good to Know

Backends improve over time. Features such as searching by links and graph views of linked traces have been appearing in some tools. Check your tool’s current documentation.

22Following Links When You Investigate

Links are only worth creating if people use them during real investigations. Here is how a typical investigation moves across them.

1

Start from a symptom

A customer reports a receipt that arrived an hour late. You find the order’s trace through the order number.

2

Notice the gap

The order trace ends after the message was published. The work after that is in another trace.

3

Search for the batch

You search for batch spans whose links point to the order’s span, or for a batch ID attribute, depending on your tool.

4

Open the batch trace

The batch span shows that it started an hour after the message, because the consumer was stuck on an earlier slow batch.

5

Follow another link

That slow batch links to a previous attempt that failed. You find the real cause, a database lock, in the failed attempt’s error.

The two directions of reading

Going forward, from a batch span to the messages it handled, is easy because the links are inside the batch span. Going backward, from a message to the batch that handled it, requires a reverse search. In practice, investigations often need the backward direction, since the symptom starts at the individual request. This is why a shared attribute, such as a batch ID that appears on both the message span and the batch span, is such a valuable companion to a link.

Everyday Analogy

Reading a book’s bibliography takes you from the book to its sources, which is easy. Finding every book that cites a certain article takes a citation index. A good scholar uses both directions, and a good tracing setup does the same.

i
Tip

Write a short runbook for your team: “To find the batch that processed a message, search for X. To find the messages in a batch, open its links.” People will use links more if the steps are written down.

23Links Compared With Events and Attributes

A span can carry several kinds of extra information. Knowing which to use for which purpose keeps spans clean.

FeatureWhat it holdsGood forNot good for
AttributeA key-value fact about the spanDescribing what the span did, such as the route or order IDPointing at another span precisely
EventA timestamped note inside the spanThings that happened at a moment, such as an exception or a retry decisionRelationships to other traces
LinkA reference to another span’s context, with optional attributesRelationships across traces, many-to-one, loose causalityDescribing details of the span itself
ParentThe span that contains this oneNested work in one traceRelationships to many spans or across traces
StatusWhether the step failedError counting and highlightingAny relationship

Mixing them well

A batch processing span could carry many links, one per message. It could carry an attribute with the batch size, an event noting that one message was skipped as invalid, and a status of Error if the batch failed. Each feature does its own job. A common mistake is to put a list of trace IDs into an attribute as text. That hides the relationship from tools that know how to read links, and it breaks attribute size limits quickly.

Use the right tool for the right data

If you need the reader to jump to another span, use a link. If you need to describe the span, use an attribute. If you need to mark a moment, use an event. Following this simple rule keeps spans understandable and tool-friendly.

Everyday Analogy

On a medical chart, the patient’s details go in the header, the timeline of events goes in the notes, and references to other patients’ linked records, such as a parent’s file, go in a “see also” field. Putting everything in one note field makes the chart hard to use.

!
Watch Out

Do not store trace IDs of related spans as an attribute string when you can use links. Tools treat the two differently, and only links can be followed by the viewer.

24Asynchronous Code Inside One Program

Links are not only for messages between services. Inside one program, asynchronous code raises the same question: parent or link?

Modern programs run work on thread pools, event loops, and futures. A request handler might start background tasks and carry on without waiting for them. A parent span ends when the handler returns, but the background task continues afterward. If the task’s span is a child, it outlives its parent, which many viewers draw in a confusing way, with a child bar extending beyond its parent’s end.

Fire-and-forget tasks

For a task that the handler starts and does not wait for, there are two reasonable choices. One is to make the task’s span a child of the handler span anyway, accepting that it ends later. This is simple and keeps the work in one trace, and it is the usual default of context propagation in many libraries. The other is to start a new trace for the task, with a link to the handler span. This gives clean bounded traces, and it is a good choice when the task is long, or when many requests feed one shared background worker.

Fan-out and join

If the handler starts ten parallel tasks and waits for all of them, the tasks are true children. The handler’s span covers them in time, and a parent relationship is right. Links are not needed. The difference is whether the parent waits. Waiting means nesting, and no waiting means you have a choice.

PatternDoes the starter wait?Recommended relation
Parallel calls then joinYesParent-child
Fire and forget, short taskNoParent-child is usually fine
Fire and forget, long or shared taskNoNew trace with a link
Background worker serving many requestsNoSpan per work item with links to the requests
Everyday Analogy

A manager who asks two employees to prepare a report and waits for both has the employees working inside the meeting. A manager who drops a request in a shared inbox and leaves has started something that will finish later, and a note saying “request came from this meeting” is enough.

i
Tip

If you see child spans that extend far beyond their parents in your viewer, you probably have fire-and-forget work that deserves a link instead.

25Cost and Performance of Links

Links are lightweight, but they are not free. Their cost grows with count and with attributes, and a careless design can surprise you.

Where the cost lives

Size

Bytes per link

A link holds a 16-byte trace ID and an 8-byte span ID, plus flags and any attributes. With attributes, tens to hundreds of bytes per link.

Count

Links per span

A batch span with hundreds of links is large. Multiply by the number of batches per second to see the total.

CPU

Extraction and creation

Extracting context from every message and creating a link adds small work per message, which is almost always negligible.

Storage

Backend indexing

Backends that index links for search store extra data. This can raise storage cost for link-heavy spans.

A worked example with made-up numbers

A consumer processes 20 batches per second, each with 100 messages. Each link takes about 60 bytes with a short attribute. A batch span therefore carries about 6,000 bytes of links, and across 20 batches that is about 120,000 bytes per second, or roughly 10 gigabytes per day. That is a noticeable but manageable amount. If the same system raised its batches to 1,000 messages and kept every link, the volume would multiply tenfold, and the team would need to reconsider.

Keeping it cheap

  • Keep link attributes short or omit them for large batches, and use a single attribute on the span instead.
  • Link only what you need. Examples may suffice for very large batches.
  • Prefer splitting batches into several spans over raising the link limit far above its default.
  • Use sampling, so that only a fraction of these spans is stored.
Everyday Analogy

A bibliography of five items is trivial. A bibliography of five thousand items on every page of a book would make the book enormous. The cost depends on how many references and how many pages.

i
Good to Know

The figures above are examples to show the arithmetic. Your own numbers depend on your serialization format, compression, and backend.

26Security and Privacy Considerations

A link is only an address, so it carries little risk by itself. The risks are in what comes along with it and in whom you choose to trust.

Links reveal relationships

A link says that two pieces of work are related. In a system that spans organizations, that can reveal who talks to whom, how often, and when. A link to an external partner’s span context shows that the partner called you. If trace data is shared with a vendor or exposed widely, these relationships are visible too. Apply the same access controls to link data as to the rest of your traces.

Untrusted contexts

As described in the trust boundary chapter, a context from an outside source should be treated as untrusted. Using it as a parent lets the outsider influence your sampling and your trace identity. Using it only as a link keeps the influence minimal, since the link does not affect which trace you belong to or whether it is sampled. Still, the content of an untrusted context is untrusted data. It might contain a trace state with strange values, so do not interpret the trace state for decisions.

Link attributes are a leak path

Link attributes can carry text. If developers put customer names, email addresses, or tokens there, the data reaches every tool that stores traces. Keep link attributes to a short fixed vocabulary, as recommended earlier. Review a sample of real links periodically.

Link floods

Consider a service that builds links from incoming contexts, for example one link per received message. An attacker who can send many messages may inflate the link counts and make spans large. The span limits protect you by capping the stored links, which is one more reason to keep the limits in place and not set them to unlimited.

Everyday Analogy

A visitor log that records which visitor came because of which referral is useful, and also sensitive. It shows relationships among people. The log should be kept in a locked drawer, and the notes written in it should not include private details.

!
Watch Out

Never copy a foreign trace state or arbitrary headers into link attributes without checking them. Treat anything from outside the trust boundary as data, never as instructions.

27Monitoring the Health of Your Links

Links break quietly. A few simple measurements tell you whether the connections you designed are really there.

Check

Dropped links count

Spans report how many links were dropped due to limits. Any steady non-zero value for important spans needs attention.

Check

Share of consumer spans with links

Expected to be near one hundred percent for link-based consumers. A drop suggests that producers stopped sending context.

Check

Dead link rate

The share of links whose targets are not found. Compare with your sampling rate to see whether it is expected.

Check

Invalid or missing context

How often a message arrives with no usable context. Rising numbers often mean a broken header path.

How to measure

Many backends let you query spans by whether they have links, and by the dropped links count. A weekly report that counts consumer spans with zero links, spans with dropped links, and links to unknown traces gives a good picture. If your backend cannot do this, a Collector component that turns span data into metrics can be configured to count spans with and without links, though the details depend on the components you run.

What to do with the numbers

A sudden drop in link coverage after a deployment is a strong clue that a library upgrade or a configuration change broke propagation. A rising dropped links count after a traffic change means that batches grew past the limit. A high dead link rate beyond what sampling explains means that the targets are not being stored for another reason, such as a pipeline problem.

Everyday Analogy

A librarian who spot-checks whether the references in the catalog lead to real shelves will quickly notice when a whole section has been moved. Without spot checks, readers discover the problem one frustrating visit at a time.

i
Tip

Add a link coverage check to the checklist for every new queue consumer. Once it is set up, it protects against silent loss of context for years.

28What Goes Wrong: Failure Scenarios

Most link problems are silent. The system keeps running and the traces keep arriving, but the connections you expected are missing or misleading.

FailureWhat you observeLikely cause
No links on consumer spansConsumer traces stand aloneProducer does not inject context, headers stripped by a broker or proxy, consumer not extracting
Links to the wrong spanLinks lead to unrelated workContext stored and reused incorrectly, such as a cached context from an earlier request
Fewer links than messagesDropped links count above zeroBatch larger than the link limit
Many dead linksClicking leads to trace not foundSampling dropped the targets, or they have expired from storage
Giant tracesTraces last hours and are cut offLong-running work nested as children, with links not used
Misleading timingChild bars far outside the parentFire-and-forget work given a parent when a link was better
Links invisible in the viewerSpans look link-freeViewer or backend does not display links, or the exporter drops them
Confusing relationshipsPeople cannot tell what a link meansNo link attributes, or inconsistent ones

The wrong-target failure deserves attention

A link that points to the wrong span is worse than no link, because it appears trustworthy. A common cause is a cached or stored context that is reused for many different jobs. For example, a service keeps the span context of “the latest request” in a shared variable and uses it later for a background job. Under concurrency, the variable may hold another request’s context at the moment the job runs. Always pass the context along with the specific work item, never through shared mutable state.

Everyday Analogy

A footnote with the wrong page number is more harmful than a missing footnote. The reader goes to the page, finds something unrelated, and may conclude that the claim is false. A wrong reference misleads, and an absent one at least invites doubt.

!
Watch Out

Do not keep span contexts in global or shared variables for later use. Attach them to the specific message, record, or task they belong to.

29Good Patterns

Teams that get value from links tend to repeat the same few habits.

Pattern

Link for many, parent for one

Use links when a span relates to several others, and use a parent when exactly one cause contains the work.

Pattern

Create links at span start

Gather the contexts first so the sampler and early processing can see them.

Pattern

Carry context with the work item

Put the span context in the message headers or in the stored record, never in shared variables.

Pattern

A relationship attribute on every link

One short attribute with a fixed vocabulary explains why the link exists.

Pattern

A business ID beside the link

An attribute such as an order number lets people find related traces even without link search.

Pattern

Fresh trace at the trust edge

Start a new trace for outside callers and link to their context.

Pattern

Phase traces with links for long processes

Cut long business flows into bounded traces and chain them with links.

Pattern

A written link policy

A short page that states when to use parent, link, or attribute, with the standard attribute names.

The value of the written policy

Links are easy to misuse because the choice between parent and link is a matter of judgment. A short written policy gives people a shared answer for the common cases. It should list each type of asynchronous flow in your system, say which relationship to use, and give the standard attribute values. A new developer then follows the policy instead of guessing.

Everyday Analogy

A newsroom style guide says how to cite sources, when to use a footnote, and when to quote in the text. Reporters follow it, and readers get a consistent experience, whichever reporter wrote the article.

i
Tip

Review the policy with the team that owns your messaging and workflow systems, because they know where the awkward cases are.

30Anti-patterns

These habits look harmless and slowly make traces less useful.

AP-1Avoid

Using a parent for everything. Nesting unrelated work produces misleading timing, huge traces, and children that outlive their parents.

AP-2Avoid

Using links as decoration. Adding links with no clear meaning or no attribute leaves readers guessing and adds size for no value.

AP-3Avoid

Ignoring the link limit. Large batches lose links silently, and nobody notices until an investigation fails.

AP-4Avoid

Putting trace IDs into attributes as text. Viewers cannot follow them, and attribute limits are hit quickly. Use real links.

AP-5Avoid

Trusting external contexts as parents. Outsiders can then steer your sampling and identifiers.

AP-6Avoid

Reusing a stored context for unrelated work. Links then point to the wrong span, which misleads investigators.

AP-7Avoid

Designing around features your viewer lacks. If your tool cannot search by links, rely also on shared attributes so that people can still find related traces.

Everyday Analogy

Anti-patterns are like citing sources without page numbers. It feels like less work when writing, and every reader pays the price later.

31Best Practices and Common Mistakes

This is the checklist to keep next to your instrumentation guide.

Best practices

  • Use a parent when one span contains and waits for another, and a link when work is related but not nested.
  • Use links for batches, for work with many causes, and for work that spans long gaps in time.
  • Create links when starting the span, so samplers and early processors see them.
  • Carry the span context with each work item, in headers or in stored records.
  • Add one short relationship attribute to each link, drawn from a small fixed vocabulary.
  • Respect the link limit, and plan for batches larger than it.
  • Add a shared business identifier beside links so that people can search even without link tools.
  • Start fresh traces at trust boundaries and link to the outside context.
  • Watch dropped links, link coverage, and dead link rates.

Common mistakes and their cures

MistakeSymptomCure
Adding the link after the span has endedThe link never appearsCreate it at start or before ending
Passing the span object instead of its context across systemsCannot serialize itUse the span context and the propagator
Forgetting to inject on the producerConsumers have nothing to linkInject the context into message headers
Creating a link from an invalid contextMeaningless zero IDsCheck validity first and skip if invalid
Linking only one message of a batch by mistakeOthers are untraceableLoop over all messages or split the batch
No attribute on the linkReaders guess the meaningAdd the relationship attribute
Everyday Analogy

Pilots read checklists not because they forget how to fly, but because skilled people also skip small steps under pressure. This list does the same for links.

i
Tip

In code review, whenever you see work that starts from a queue, a schedule, or a callback, ask what the relationship to the cause is, and whether it is a parent, a link, or only an attribute.

32Real-World Style Examples

These stories are invented to illustrate common situations. They are not reports about real companies.

Example one: the slow receipts

A shop’s receipts sometimes arrive an hour late. Each order’s trace ends when the message is published, and nothing explains the delay. The team changes the receipt consumer to start a batch span for each group of messages, with a link to each order’s span and a batch ID attribute. A search for the batch of a late receipt shows that batches wait behind one slow batch that is blocked on a database lock. The team fixes the lock, and delays vanish.

Example two: the partner who flooded the traces

A partner’s integration sends all requests with a sampled flag set, causing a surge in stored traces. The team changes the public gateway to start a fresh trace for each external request and to link to the partner’s context. Volume returns to normal, and when the partner reports a problem with a trace ID, the team finds the linked span quickly through the link.

Example three: the week-long order

An order trace stays open for a week as payment, shipping, and delivery steps join it, and the backend truncates it. The team splits the flow into phase traces, each linked to the previous one and each carrying the order ID. Traces are short and complete, and customer support finds the whole story by searching for the order number and following the links.

Example four: the batch that outgrew the limit

A consumer’s batches grow from 50 to 400 messages after a traffic increase. Engineers find that many orders cannot be traced to their batch. The dropped links count on batch spans reads 272. The team splits batches into groups of 100 for tracing purposes and adds the batch ID attribute to every message span. Every order can be found again.

Everyday Analogy

Each story is like a lesson from a driving instructor: one rule, one mistake, one fix. The same few lessons repeat for many drivers on many roads.

i
Good to Know

All four examples are fictional and simplified. In real incidents, several causes usually overlap, so confirm your diagnosis with measured data.

33Troubleshooting: My Links Are Missing or Wrong

When links do not behave, follow the path of the context from the producer to the viewer. The first break you find is the cause.

flowchart TB
    START["Expected link is missing or wrong"] --> A{"Does the producer inject context into the carrier?"}
    A -- "no" --> A1["Enable propagation or instrumentation on the producer"]
    A -- "yes" --> B{"Does the context survive the broker or proxy?"}
    B -- "no" --> B1["Check header names, filters, and bridges"]
    B -- "yes" --> C{"Does the consumer extract a valid context?"}
    C -- "no" --> C1["Fix extraction or the propagator format"]
    C -- "yes" --> D{"Is the link added when the span starts?"}
    D -- "no" --> D1["Create the link at span start or before the span ends"]
    D -- "yes" --> E{"Is dropped links count above zero?"}
    E -- "yes" --> E1["Raise the limit or split the batch"]
    E -- "no" --> F{"Does the viewer show links?"}
    F -- "no" --> F1["Check exporter, backend support, and the viewer"]
    F -- "yes" --> F2["Check whether the target trace was sampled"]
Figure 11: A checklist that follows the span context from the producer to the viewer

Explaining the checklist

Begin at the producer, because without an injected context nothing else can work. Check that the sending side’s instrumentation is on, and that the propagator format matches what the consumer expects. Next, check the path between the two. Brokers, proxies, and bridges between messaging systems sometimes drop or rename headers, which silently removes the context. Then check the consumer: does it extract the context, and is the result valid, with non-zero identifiers?

If the context arrives, check how the link is created. It must be added when the span starts or before it ends. After that, check for dropped links, which shows when the batch is bigger than the limit. Finally, check the display: whether your exporter sends links, whether the backend stores them, and whether the viewer shows them. If all of that is fine and the link still leads nowhere, the target trace may have been dropped by sampling.

Everyday Analogy

If a reference slip does not lead you to the book, you check whether the slip was written, whether it survived being copied, whether the number was read correctly, and finally whether the library still owns the book. You follow the slip’s journey in order.

i
Tip

Temporarily log the extracted context on the consumer in a test environment. Seeing the actual trace ID and span ID there immediately shows whether the propagation chain is intact.

34How It Looks in Different Languages

The idea of a link is the same everywhere. The way you reach it in code looks a little different in each language.

The shared shape

Every OpenTelemetry API lets you pass a list of links when you start a span. Each link is built from a span context, which you obtain either from a span object you hold, or from a context you extracted from a carrier, and an optional set of attributes. The API also provides helper functions to build a span context from stored identifiers, which is how links work for scheduled and retry cases. Newer API versions add a method to add a link to a span after it has started.

Where languages differ

TopicWhat varies
NamingEach language uses its own style for the link type and the starting options that carry links
Adding links after startAvailable in newer versions of some APIs, and absent or experimental in others
LimitsDefault and configuration names differ slightly, though most follow the common default of 128 links
Messaging instrumentationLibraries differ in whether they use parent, link, or both for consumers, and may offer a setting to choose
Validity helpersFunctions to check that a span context is valid exist in each API, with different names

How to check your language

Find the tracing page in your language’s documentation, and look for the section about span creation and links. Then write a small test that creates a producer span, extracts its context, starts a consumer span with a link, and prints the result using a console exporter. In a few minutes you will know how links behave in your SDK and how your viewer shows them.

Everyday Analogy

Footnotes look different in different publishing programs, with numbers or symbols, at the page bottom or at the end of a chapter, and the idea of “see also” remains the same. Learn the idea once, and look up the local style when you need it.

!
Watch Out

Do not assume that an example from one language works the same in another. Check especially whether your version supports adding links after the span starts, and what the default limits are.

35Frequently Asked Questions

Q1 What is a span link in OpenTelemetry?

A span link is a reference from one span to another span’s context, made of a trace ID, a span ID, and optional attributes. It records a relationship without making the linked span a parent, and it can cross traces.

Q2 How is a link different from a parent?

A span has at most one parent, in the same trace, and the child runs within the parent’s work. A span can have many links, to spans in any traces, and no nesting or timing is implied.

Q3 When should I use a link?

Use links when a span relates to several others, as in batch processing, when work happens long after its cause, when a trace would become huge, when you start a fresh trace at a trust boundary, or when linking retries and shared work.

Q4 Does the linked span know about the link?

No. Links are stored on the linking span only. Finding spans that link to a given span needs a reverse search, which depends on your backend.

Q5 Can I add a link after the span starts?

Newer API versions allow it, but links created at start are better because samplers see them. After the span ends, nothing can be added.

Q6 How many links can a span have?

The common default limit is 128, configurable by SDK settings. Extra links are dropped, and the span records the dropped count. Check your SDK’s defaults.

Q7 Do links affect sampling?

Samplers receive the links at span start and may use them. Built-in samplers ignore them. Each trace is sampled independently, so a link can lead to a trace that was dropped.

Q8 How does the consumer get the context to link to?

The producer injects the span context into message headers with a propagator, and the consumer extracts it. For scheduled or retried work, store the trace ID and span ID in your own records and rebuild a span context from them.

Q9 Should message consumers use links or parents?

For one message handled promptly, a parent is simple. For batches, long delays, or fan-in, links fit better. Check what your instrumentation does by default, and keep your system consistent.

Q10 Why start a new trace at my public gateway and link to the caller?

It stops outside callers from steering your trace IDs and sampling, while keeping a reference to their context for support and debugging.

Q11 What attributes should a link carry?

A single short attribute naming the relationship, such as batch member or previous attempt, from a small fixed set, and nothing sensitive.

Q12 My viewer does not show links. What now?

Check whether the exporter and backend preserve links. If the tool lacks support, add a shared business ID attribute, such as an order or batch ID, so people can find related traces by search.

36Summary and Key Takeaways

A span link is a lightweight pointer from one span to another that records a relationship without nesting. It holds the target’s trace ID and span ID, plus optional attributes, and it can cross trace boundaries. Links exist because the tree of parent and child cannot express many-to-one relationships, long delays, untrusted callers, or shared work. They are used for batch processing, messaging, retries, scheduled work, trust boundaries, and very long processes. To use them well, create them when the span starts, carry the context with the work item, add a short relationship attribute, respect the limits, and back them up with shared business identifiers, since viewers vary in how well they support links.

Key Takeaways

  • A span link points from one span to another span’s context, with optional attributes. It does not create nesting and may cross traces.
  • A parent means “inside and waiting for”. A link means “related to”. Use links for many-to-one, long gaps, untrusted sources, and shared work.
  • The link lives on the linking span only. Searching backward needs a backend feature or a shared attribute such as a batch or order ID.
  • Create links at span start so samplers can use them, and carry the span context with each work item through headers or stored records.
  • Default limits, commonly 128 links per span, drop extra links silently. Split large batches, raise limits with care, and watch the dropped links count.
  • Sampling treats traces independently, so some links will be dead. Plan for it, or sample link-heavy paths more generously.
  • Keep link attributes short and private, test how your viewer displays links, and write a team policy for choosing parent, link, or attribute.