What Is a Parent-Child Span Relationship in OpenTelemetry?

What Is a Parent-Child Span Relationship in OpenTelemetry?

A plain-English guide to the single idea that turns a pile of timing records into a readable story: which piece of work caused which other piece of work, and how long each one took.

Imagine you ask a restaurant for a pizza and it takes forty minutes. Somebody tells you, “The kitchen was slow.” That is not very helpful. Was the dough late? Was the oven full? Did the delivery driver get lost? To fix the problem, you need to know the chain of events: the order came in, the kitchen started the dough, the dough waited for the oven, the pizza went to the driver. Each step belongs to a bigger step, and each bigger step belongs to the whole order.

Software has exactly the same problem. One click on a “Buy” button can wake up a dozen services, and each of them does several smaller jobs. OpenTelemetry records each job as a span. The thing that connects the jobs into one understandable story is the parent-child relationship between spans.

In this guide, you will learn what that relationship is, how OpenTelemetry stores it, how the SDK decides who the parent is, how it crosses from one service to another, and the mistakes that quietly break it. No prior knowledge is needed. Every new word is explained the first time it appears.

1What Is a Parent-Child Span Relationship?

The idea is simple, and once you see it, you will notice it everywhere. A span can start other spans while it is still running, and the span that started the work is called the parent.

Start with the word “span”

A span is one timed piece of work. It has a name, a start time, an end time, and some labels. “Handle the checkout request” is a span. “Run this database query” is a span. “Call the payment service” is a span.

Now add the relationship

When one piece of work starts another piece of work, the first span becomes the parent and the new span becomes its child. The child is a smaller step done on behalf of the parent. The parent usually waits for its children to finish before it can finish itself, although there are important exceptions that we will cover later.

A span with no parent is called the root span. It is the very first piece of work in the story, often the arrival of a user request. All the spans that belong to the same story, linked by parent-child connections, form a trace. Because every span has at most one parent, the trace is shaped like a family tree turned upside down, with the root at the top.

Everyday Analogy

Think of a company’s org chart. The chief executive has no boss, so she is the root. She has managers who report to her, and each manager has team members who report to them. If you want to know who is responsible for a task, you follow the lines upward. A trace is an org chart for one request, where each box is a piece of work instead of a person.

A small tree you can picture

Look at the diagram below. A checkout request arrives, and the service does three things: it validates the cart, it charges the card, and it saves the order. Charging the card means calling another service, which itself does a fraud check.

flowchart TB
    R["POST checkout (root span, SERVER, Order Service)"]
    V["validate cart (INTERNAL, Order Service)"]
    C["charge card (CLIENT, Order Service)"]
    P["POST charge (SERVER, Payment Service)"]
    F["fraud check (INTERNAL, Payment Service)"]
    S["save order (CLIENT, Order Service)"]
    D["INSERT order (database call)"]
    R --> V
    R --> C
    C --> P
    P --> F
    R --> S
    S --> D
Figure 1: One checkout request as a tree of parent and child spans

Read the arrows as “started”. The root started three children. The “charge card” span started the span in the payment service, which started a fraud check. Two spans that share the same parent, such as “validate cart” and “save order”, are called siblings.

1
usual root span per trace, with no parent
1
parent at most for every other span
0+
children any span may have
i
Tip

When you are confused by a trace, ask one question about any span: “Who started you?” The answer is its parent. Follow that answer upward until you reach the root.

2Why the Relationship Matters

Timing numbers alone are almost useless. The relationship between spans is what gives those numbers meaning.

A flat list tells you very little

Suppose a tool gave you only a plain list: “validate cart, 20 ms”, “charge card, 900 ms”, “fraud check, 850 ms”, “save order, 40 ms”. You would see big numbers, but you could not tell whether they add up, overlap, or belong to different requests. Is the fraud check part of the card charge, or a separate slow step? You would be guessing.

With the parent-child links, the answer is obvious. The fraud check is a child of the card charge. The card charge took 900 milliseconds, and 850 of those were spent inside the fraud check. The place to look is no longer a mystery.

Everyday Analogy

A bank statement with only a list of amounts is hard to read. A statement that groups each purchase under the trip or event it belonged to tells a story: “Holiday: flights, hotel, meals.” The grouping is the meaning. Parent-child links are the grouping for your software’s work.

Questions the relationship lets you answer

Cause

What caused this work?

Follow parents upward to find the request, job, or event that started a slow or failing step.

Time

Where did the time go?

Compare a parent’s duration with its children’s durations to find the step that used most of it.

Blame

Which service is at fault?

Because spans carry the service that produced them, a failing child shows which service to investigate.

Shape

How is the system really wired?

Many tools draw service maps by looking at which services’ spans are children of which others.

Failure

Did an error spread upward?

You can see whether a child’s error became the parent’s error, or whether the parent handled it and carried on.

Waste

Is work repeated or too serial?

A parent with fifty sequential children, all calling the same database, is a clear sign of an avoidable slowdown.

A short history

The tree-of-spans idea did not start with OpenTelemetry. It was popularized by a 2010 paper from Google about an internal tracing system called Dapper. Open-source tools such as Zipkin were inspired by it. Later, the OpenTracing project described relationships with two reference types, “child of” and “follows from”. When OpenTracing and OpenCensus merged into OpenTelemetry in 2019, the model kept the parent-child tree and replaced “follows from” with a more flexible feature called span links, which you will meet in Chapter 9.

3Core Vocabulary: The Words You Need

A few simple terms come up again and again. Learn them now, and the rest of the guide will feel easy.

TermPlain meaningExample
TraceThe whole story of one request, made of many spansOne checkout from click to confirmation
SpanOne timed piece of work inside the story“charge card”
Root spanThe span with no parent, the start of the treeThe incoming web request
Parent spanThe span that started another span“charge card” is the parent of the payment service’s span
Child spanA span started by another span“fraud check” is the child of the payment service’s span
Sibling spansSpans that share the same parent“validate cart” and “save order”
AncestorA parent, a parent’s parent, and so on upwardThe root is an ancestor of every span
DescendantA child, a child’s child, and so on downward“fraud check” is a descendant of the root
Leaf spanA span that has no children“INSERT order”

Parent is not the same as caller, exactly

In everyday speech we say “service A calls service B”. In tracing, we get more precise. The client span inside service A, which represents the outgoing call, is the parent of the server span inside service B, which represents handling that call. The parent-child link is between spans, not between whole services.

Depth and breadth

The depth of a tree is how many levels of parents and children it has, and the breadth is how many children a single parent has. A deep tree usually means a long chain of services calling services. A wide tree often means one span doing many small things, or fanning out to many parallel calls. Both shapes are worth noticing because each points to different performance risks.

i
Good to Know

The relationship is one parent per span. A span cannot have two parents. If one piece of work is caused by several earlier pieces of work, OpenTelemetry uses links to show the extra connections.

4How OpenTelemetry Stores the Link

There is no magic here. The relationship is stored as a few plain IDs, and a backend uses them to rebuild the tree.

Three IDs do all the work

Shared by all

Trace ID

A long random number, shown as 32 hexadecimal characters. Every span in the same trace carries the same trace ID. It says “we all belong to the same story”.

Unique to one

Span ID

A shorter random number, shown as 16 hexadecimal characters. Each span has its own span ID. It says “this is me”.

The link

Parent span ID

The span ID of the span that started this one. It says “this is who started me”. A root span leaves it empty.

That is the entire mechanism. A child span copies the trace ID from its parent, makes a new span ID for itself, and records its parent’s span ID in the parent field. A backend that receives thousands of spans can group them by trace ID, then connect them by matching each parent span ID to some other span’s own ID.

Everyday Analogy

Think of a family reunion where every person wears a name tag. The tag has a family code, which is the same for everyone and works like the trace ID. It also has the person’s own number and the number of their mother or father. With only those tags, a stranger could draw the whole family tree without asking anyone a question.

The span context: a tiny passport

The part of a span that is safe to pass around is called its span context. It is small and unchangeable. It contains:

  • the trace ID,
  • the span ID,
  • trace flags, including a bit that says whether the trace is being sampled,
  • optional trace state, a place for vendor-specific hints,
  • a marker that says whether the context came from a remote service.

The parent’s span context is exactly what a child needs to be born. Nothing else about the parent, such as its attributes or its end time, has to be known by the child. This is why the relationship can cross network borders so easily: only the small passport needs to travel.

Local parent and remote parent

If the parent runs in the same process as the child, it is a local parent. If the parent’s context arrived through an incoming message, such as an HTTP header, it is a remote parent. The span context records this difference with its remote marker. Remote parents are what stitch many services into one trace.

FieldRoot spanChild span
Trace IDNewly createdCopied from the parent
Span IDNewly createdNewly created
Parent span IDEmptyThe parent’s span ID
Sampling decisionMade by the samplerUsually follows the parent

5How the SDK Chooses a Parent

Your code rarely says “this span’s parent is span number 7”. Instead, the SDK works out the parent for you by looking at something called the context. Understanding this is the key to fixing most broken traces.

Context: who is the “current” span?

Context is a small invisible package that follows your code as it runs. One of the things it holds is the active span, the span that is currently “in progress” for this line of execution. When your code starts a new span and does not say otherwise, the SDK looks into the current context, finds the active span, and uses it as the parent.

Everyday Analogy

Imagine a notebook where you write meeting notes. Each time a new topic starts, you open a fresh page and write the heading under the current section. The “current section” is the active span. If you forget to open the section, new notes land under whatever was there before, or in a random place.

The decision, step by step

flowchart TB
    A["Code asks the tracer to start a span"] --> B{"Was a parent context given explicitly?"}
    B -- Yes --> C["Use that context to find the parent"]
    B -- No --> D{"Is there an active span in the current context?"}
    D -- Yes --> E["Make the active span the parent"]
    D -- No --> F["Create a root span with a new trace ID"]
    C --> G["Child copies trace ID and records parent span ID"]
    E --> G
Figure 2: How the SDK decides on a parent when a span is started

There are three outcomes. If you pass a context explicitly, the SDK uses it. If you do not, it uses the active span from the current context. If neither exists, the new span becomes a root and starts a brand-new trace. You can also deliberately force a root span by starting it with an empty context, which is useful when a background job should not be tied to the request that happened to trigger it.

Starting a span is not the same as making it active

This is the most common surprise for beginners. In OpenTelemetry’s design, simply creating a span does not automatically make it the active span. A second step, often called “make current” or “activate”, puts it into the context. Most languages give you a helper that does both steps at once, such as a “start as current span” style function. If you create a span but never activate it, the next span you create will not see it, and will attach to some older parent or become a root.

!
Watch Out

If your trace looks flat, with many sibling spans that should have been nested, a likely cause is that the parent span was created but never made current. The children simply never saw it.

How each language carries the context

Languages store the current context in different ways, and this explains many language-specific bugs.

Language styleHow context travelsWhat can go wrong
GoYou pass the context object explicitly through function callsForgetting to pass it creates a new root
Java and .NETHeld in thread-local or async-local storageCustom thread pools may not carry it across
PythonHeld in context variablesRaw threads may start with an empty context
JavaScript (Node.js)Held in async-local storage tied to async callsSome callback or event-emitter styles lose it
i
Tip

Automatic instrumentation agents usually wrap common thread pools and async libraries so that context survives. If you write your own concurrency code, check that the context is handed over to the new worker.

6Crossing Service Borders: Remote Parents

Inside one program, context lives in memory. Between two services, it must be sent over the network. This is where a trace either becomes a beautiful end-to-end story or falls into pieces.

Inject and extract

When service A calls service B, a component called a propagator writes the current span context into the outgoing request, usually as HTTP headers. This step is called inject. When service B receives the request, its propagator reads those headers and rebuilds the span context. This step is called extract. The extracted context becomes the remote parent of the first span that service B creates.

The default format is the W3C Trace Context standard, which uses a header named traceparent. It has four dash-separated parts: a version, the trace ID, the parent ID, and flags. That third part is the span ID of the caller’s span. Its name in the standard, “parent ID”, tells the whole story: it is the identity of the parent that the receiver should use.

Everyday Analogy

A relay race has a baton. The runner hands it to the next runner, and the next runner knows exactly who handed it over. The traceparent header is the baton. It carries “the race number” (trace ID) and “the runner who just handed this over” (parent ID).

One request, three services

sequenceDiagram
    participant U as User Browser
    participant G as API Gateway
    participant O as Order Service
    participant P as Payment Service
    U->>G: HTTP request
    Note over G: Creates root SERVER span S1
    G->>O: Call with trace T1 and parent ID S2 (client span)
    Note over O: Creates SERVER span S3 with remote parent S2
    O->>P: Call with trace T1 and parent ID S4 (client span)
    Note over P: Creates SERVER span S5 with remote parent S4
    P-->>O: Response
    O-->>G: Response
    G-->>U: Response
Figure 3: Every hop uses the same trace ID, and each hop’s parent is the caller’s client span

Follow the IDs. The trace ID, T1, never changes. At every hop, the caller creates a client span, and its span ID is placed in the header. The receiver creates a server span whose parent is that client span. So the chain is: gateway root span, gateway client span, order service server span, order service client span, payment service server span. Each service writes its own spans and sends them to the backend on its own. The backend later joins them through the IDs.

Why parent-child works across languages and vendors

Because the header format is a public standard, the gateway can be written in one language, the order service in a second, and the payment service in a third. As long as each one reads and writes the same header, the tree stays connected. This is one of the biggest practical gifts of OpenTelemetry to teams with mixed technology stacks.

When the chain breaks

1

A proxy drops the header

A gateway or load balancer that does not forward unknown headers cuts the chain. The next service starts a brand-new trace and a fresh root.

2

A service has no instrumentation

A service that never extracts or injects context breaks the link in the middle. Spans before and after look unrelated.

3

Different header formats

One service writes the W3C format, another only reads an older format. Neither understands the other’s headers.

4

Context lost inside a service

Code switches threads or tasks and forgets to hand over the context, so outgoing calls carry no parent.

7Span Kinds: The Role Each Span Plays

Every span carries a label called its kind. The kind does not change the parent-child rule, but it tells tools what role the span plays, and that helps them draw correct pictures.

Receiving

SERVER

Handles an incoming request from outside the service, such as an HTTP request arriving. Its parent is usually a remote client span.

Sending

CLIENT

Represents an outgoing request to another service or database. It waits for the answer. Its child is usually a remote server span.

Inside

INTERNAL

Work that stays inside the service, such as validating data or running a calculation. Its parent and children are normally in the same process.

Messaging

PRODUCER

Represents putting a message on a queue or topic. It does not wait for the message to be processed.

Messaging

CONSUMER

Represents receiving or processing a message from a queue. It may be linked to the producer that sent the message.

The natural pairs

Kinds usually come in pairs across a border. A CLIENT span in one service is the parent of a SERVER span in the next. A PRODUCER span is paired with a CONSUMER span. INTERNAL spans fill the space in between. When tools see these pairs, they can safely say “service A called service B”, and they can measure network time as the gap between the end of the server span and the end of the client span.

flowchart LR
    CL["CLIENT span (Order Service)"] -- parent of --> SV["SERVER span (Payment Service)"]
    SV -- parent of --> IN["INTERNAL span (fraud check)"]
    PR["PRODUCER span (Order Service)"] -. message carries context .-> CO["CONSUMER span (Email Service)"]
Figure 4: Common pairings of span kinds across a service border
Everyday Analogy

A phone call has a caller and a receiver. The caller’s phone log shows “outgoing call, 4 minutes”. The receiver’s log shows “incoming call, 3 minutes 50 seconds”. Comparing the two logs reveals the delay in connecting. A client span and its server child are those two logs for a network call.

i
Tip

Use the right kind when you create spans by hand. A database call should be CLIENT, not INTERNAL. Wrong kinds lead to missing arrows on service maps and wrong latency math.

8Reading a Trace: Time, Nesting, and the Critical Path

Most tracing tools show a trace as a waterfall, a stack of horizontal bars. Parent-child relationships decide how those bars are indented and how you should read them.

The shape of a waterfall

Each bar is one span. Its left edge is the start time and its length is the duration. Children appear indented under their parent and usually sit inside the parent’s time range. Sibling bars that sit one after another show sequential work. Sibling bars that overlap show parallel work.

An example with real numbers

Say the “POST checkout” root span takes 300 milliseconds. Here is how its children could be laid out.

SpanParentStart (ms)Duration (ms)
POST checkoutnone (root)0300
validate cartPOST checkout1020
charge cardPOST checkout40200
fraud checkcharge card60150
save orderPOST checkout25040

Self time: what the parent did by itself

A parent’s own work is called its self time. It is the parent’s duration minus the time covered by its children. For “charge card”, the duration is 200 ms and the fraud check child covers 150 ms, so its self time is about 50 ms. That 50 ms is spent in the card-charging span itself, for example preparing the request and waiting on the network. For the root, the children cover 20 + 200 + 40 = 260 ms of the 300 ms, leaving about 40 ms of self time spent between steps.

If children overlap, you should not add their durations. Count only the time covered by at least one child. Tools calculate this for you, but knowing the rule helps you avoid wrong conclusions.

Everyday Analogy

You plan a dinner party that takes three hours in total. Cooking takes two of those hours, setting the table takes thirty minutes, and the rest is greeting guests. Your own time, or self time, is the greeting. The tree of jobs lets you see which job ate the evening.

The critical path

The critical path is the chain of spans that determines the total length of the trace. In our example, “validate cart”, then “charge card” with its fraud check, then “save order” run one after another, so all of them sit on the critical path. If two siblings run in parallel, only the longer one is on the critical path, because shortening the shorter one would not make the request faster. Always optimize spans on the critical path first.

Surprises you may see

  • A child that starts before its parent. Different machines have slightly different clocks. A small clock difference can make a child look as if it began before its parent. This is called clock skew, and it is normal across services.
  • A child that ends after its parent. In asynchronous work, a parent may finish and return a response while a child keeps going in the background. This is allowed.
  • A parent much longer than its children. This means time was spent in the parent itself or in uninstrumented code. It is a hint to add more spans.
i
Good to Know

The tree describes who started whom. It does not promise that a parent always lasts longer than its children. Never assume the time ranges nest perfectly.

9Beyond the Tree: Span Links

Real systems are not always shaped like clean trees. Sometimes one piece of work is caused by several earlier pieces, or by something from a completely different trace. For these cases OpenTelemetry offers links.

What is a link?

A span link is a pointer from one span to another span’s context, without making it the parent. The link says “I am related to that span, but I was not started by it”. A link may point to a span in the same trace or in a different trace, and it can carry its own small set of attributes to explain the relationship.

Everyday Analogy

A child has exactly one mother who gave birth to her. That is the parent link. But the child may also have a pen friend from another country, a teacher, and a cousin. Those are connections, but none of them is a parent. Links record the connections that are not parenthood.

When to use a link instead of a parent

flowchart TB
    M1["Message 1 (trace A)"] -. link .-> B["Batch job span (new trace)"]
    M2["Message 2 (trace B)"] -. link .-> B
    M3["Message 3 (trace C)"] -. link .-> B
    B --> W["write results to database (child of batch job)"]
Figure 5: A batch span with a single parent-child tree below it and links back to three different traces

Imagine a job that collects one hundred messages from a queue and processes them together as a batch. Each message came from a different original request, so each belongs to a different trace. A batch span cannot have one hundred parents. Instead, the batch span starts its own trace and holds one hundred links, each pointing to the message that contributed to it. Someone investigating a slow message can follow the link to the batch, and someone investigating the batch can follow the links back to the messages.

SituationUse a parentUse a link
A function calls another function in the same requestYesNo
One service calls another and waitsYesNo
A batch job processes many messages from different requestsNo single parentYes, one per source
A scheduled job started by something that is already finishedSometimesOften yes
A retry that wants to refer to a failed earlier attemptRetry may be a child of the same operationOptional link to the earlier attempt
A user session that spans many unrelated requestsNoSometimes, to connect traces
i
Tip

Use a parent when the relationship means “caused and waited for”. Use a link when it means “is related to”. If you are unsure, ask whether the second span could exist without the first one having started it directly.

10Threads, Async Work, and Concurrency

Modern programs do many things at once. When work moves between threads or tasks, the context that holds the active span has to move too. If it does not, parent-child links quietly vanish.

The problem in plain words

Think of the context as a sticky note placed on a worker’s desk. When the worker hands part of the job to a colleague at another desk, the sticky note stays behind unless someone copies it. The colleague then starts a new span, looks at the desk, finds no note, and creates a root span. The trace splits in two, and nobody knows why.

Case one

Parallel children

A parent starts three tasks at once and waits for all. Each task should start a child of the same parent. The result is three overlapping sibling bars, which is correct and informative.

Case two

Fire and forget

A request starts a background task and returns right away. The task’s span is still a child of the request span, but it ends after the parent. That is valid and often useful.

Case three

Thread pools

Work submitted to a pool runs later on a different thread. Context must be captured when the work is submitted and restored when it runs.

Case four

Callbacks and events

Event-driven code may run a handler long after the original call returned. Pass the context along, or the handler will start without a parent.

How to keep the link

  • Prefer the language’s built-in async features that carry context automatically, where they exist.
  • Use the SDK’s context-wrapping helpers for thread pools and task runners, or the automatic agent that does it for you.
  • In languages that pass context explicitly, make passing the context object a habit in every function that does traced work.
  • When work will clearly outlive the request and should not be a child, start a new root span and add a link back to the originating span.
!
Watch Out

A “fire and forget” child span that ends long after its parent can confuse people reading the trace. Mention it in the span name or attributes, such as “async cleanup”, so a reader does not think something is broken.

11Queues and Messaging: Parent or Link?

Message queues break the usual rule that the caller waits. The sender drops a message and moves on, and the receiver may pick it up seconds or hours later. This makes the parent-child question more interesting.

How context travels through a message

HTTP has headers. Messages have their own header-like fields, often called properties or attributes. When a service sends a message, the propagator injects the span context into those fields. When another service receives the message, it extracts the context from them. The idea is the same as with HTTP, but the time gap between sender and receiver can be large.

Everyday Analogy

Mailing a letter is different from making a phone call. When you post a letter, you write your return address on the envelope. The receiver may read it next week, but they can still see who sent it. The injected trace context is the return address on the envelope.

Two common shapes

ShapeWhat happensRelationship to use
One message, one consumer actionThe consumer processes a single message and does its workThe consumer’s processing span can be a child of the span that sent the message
Many messages in one batchThe consumer pulls many messages togetherThe batch span links to each message’s context
Fan-outOne message goes to many subscribersEach subscriber’s processing span is a child of the same producer span
Long delay or scheduled deliveryThe message is processed much laterOften a new trace with a link, to keep traces from becoming huge

The semantic conventions for messaging describe which spans to create and how to connect them, and they have been refined over time. Check the current version for your messaging system before choosing, and remember that the choice is partly a design decision about how long you want one trace to last.

i
Good to Know

A trace that keeps growing for hours is hard to read and expensive to store. Use links to start fresh traces at natural boundaries, such as after a queue with long delays.

12How It Works Internally: From Span Start to a Rebuilt Tree

Now let us follow what actually happens, step by step, from the moment a child span is created to the moment a dashboard draws the tree.

1

The parent span is active

A server span has been started for an incoming request and made current, so the context holds its span context.

2

Code starts a child

Your code, or a library, asks a tracer to start a span. The SDK looks up the parent in the context.

3

IDs are assigned

The child copies the trace ID, receives a fresh span ID from the ID generator, and stores the parent’s span ID in its parent field.

4

Sampling is applied

The sampler is consulted. A parent-based sampler normally follows the decision already stored in the parent’s flags, so the whole tree is kept or dropped together.

5

The child runs and ends

Attributes, events, and status are added. When the child ends, the SDK records the end time and hands the finished span to the span processor.

6

The parent ends later

Children usually end first, so they are queued for export before their parent. The order of arrival is therefore bottom-up and not top-down.

7

Spans are exported separately

Each service batches and sends its spans on its own schedule, with no coordination. Spans of one trace arrive from many sources, at different times, in any order.

8

The backend rebuilds the tree

The backend groups spans by trace ID and connects them by matching each parent span ID to another span’s own ID. Any span whose parent has not arrived yet waits, or is shown as a top-level span until the parent shows up.

flowchart LR
    A["Gateway SDK (spans S1, S2)"] --> CO["Collector"]
    B["Order Service SDK (spans S3, S4)"] --> CO
    C["Payment Service SDK (span S5)"] --> CO
    CO --> BE["Backend: group by trace ID"]
    BE --> TR["Rebuild tree by matching parent span IDs"]
    TR --> UI["Waterfall view for engineers"]
Figure 6: Spans from many services meet in the backend, where parent IDs rebuild the tree

Why no service needs the whole tree

This design is clever because every service knows only two things: its own span, and the ID of the span that started it. No service has to know how deep the tree is, how many services are involved, or who sits above its parent. This keeps the work small and the system scalable. The full picture only exists at the end, in the backend.

Orphans: spans whose parent never arrived

If a parent span is lost, because it was dropped, never exported, or still waiting in a queue, its children become orphans. Tools handle orphans in different ways. Some show a placeholder where the parent should be, and some show the orphan as a top-level span. A growing number of orphans is a clear sign of lost data or broken export.

i
Tip

A trace that shows several top-level spans instead of one root is not always a bug in your code. First check whether spans from one service are missing or late. Telemetry arrives out of order by design.

13A Worked Example: Rebuilding a Tree from Raw Records

The best way to trust an idea is to do it yourself on paper. Here is a tiny set of span records, and you will rebuild the tree exactly the way a backend does.

The raw records

Five spans arrive at a backend, in no special order. To keep the numbers short, we write the IDs as simple names instead of long hexadecimal strings. Every record has the same trace ID, T1, so we know they belong together.

Arrival orderSpan nameSpan IDParent span IDService
1fraud checkS5S4Payment Service
2POST chargeS4S3Payment Service
3charge cardS3S2Order Service
4POST checkoutS2(empty)Order Service
5validate cartS6S2Order Service

The rebuilding steps

1

Find the root

Look for the record whose parent field is empty. That is “POST checkout”, with span ID S2. It sits at the top of the tree.

2

Find its children

Search for every record whose parent is S2. Two spans match: “charge card” (S3) and “validate cart” (S6). They are siblings under the root.

3

Go one level deeper

Which records have S3 as a parent? Only “POST charge” (S4). It belongs to a different service, which tells us the call crossed a service border.

4

Keep going until nothing is left

Which records have S4 as a parent? “fraud check” (S5). Nothing has S5 as a parent, so S5 is a leaf, and the tree is complete.

Notice what just happened. The records arrived in the opposite order from the tree, with the deepest span first and the root fourth. It did not matter. The parent IDs alone were enough to rebuild the structure. Notice also what is missing: no single record knew the whole tree. Each knew only its own ID and its parent’s ID.

What if one record never arrives?

Remove the record for “charge card” (S3) from the table. Now “POST charge” (S4) points to a parent, S3, that does not exist. A backend would treat S4 as an orphan, and the payment branch would float disconnected from the rest. The tree does not crash, but it shows a gap, and the gap itself is a clue that data was lost or is late.

Everyday Analogy

It is like a stack of index cards, each with a person’s name and their mother’s name. You can shuffle the cards and still draw the whole family tree. If one card is lost, you will see a child pointing to a mother you cannot find. You know exactly who is missing, even if you do not know anything else about her.

i
Tip

When a trace looks wrong, write down the span IDs and parent IDs of the spans you can see, and try the rebuilding steps by hand. The first record whose parent cannot be found shows where the break is.

14Logs, Metrics, and the Tree: Tying All Signals Together

Traces are not alone. Logs and metrics describe the same system, and the span relationship is the thread that can connect them all.

Logs that know their span

When your code writes a log message while a span is active, the SDK can copy the current trace ID and span ID onto the log record. The log line is then pinned to one exact box in the tree. If a payment fails, you can open the failing child span and read only the log lines written inside it, instead of searching through a million unrelated lines. In the other direction, you can start from an error log and jump up to the whole trace that produced it, seeing the parents that led to the failure.

Logs

Trace and span IDs on every line

Turn on log correlation in your logging setup. Each log record carries the IDs of the span that was active when it was written.

Logs

Span events for key moments

For a few important moments, such as “retry started”, add an event to the current span. The event lives inside the span, close to its cause.

Metrics

Exemplars

A metric point, such as a slow-request bucket, can carry the ID of an example trace. You click from a chart spike straight into a tree that explains it.

Metrics

Metrics derived from spans

A Collector can compute counts and durations from spans by service and operation. Remember that sampling affects these numbers, so SDK metrics are safer for exact rates.

Choosing the right signal for each question

QuestionBest signalWhy
Is the error rate rising right now?MetricsCheap, complete, and ideal for alerts
Which service made this one request slow?TracesThe parent-child tree shows where time went
What exactly did the code say at that moment?Logs, linked to the spanDetailed text that is placed in context by the span IDs
Why is the 99th percentile worse after the release?Metrics plus exemplars, then tracesThe chart finds the symptom, and the linked trace explains the cause
Everyday Analogy

A hospital uses a patient’s chart number to connect blood tests, doctor’s notes, and heart-rate readings. The chart number turns scattered facts into one medical story. In observability, the trace ID and span ID are the chart number for your system.

!
Watch Out

Do not put trace IDs or span IDs on metric labels. Each trace has a unique ID, so labels would explode into millions of time series. Exemplars exist precisely to link metrics to traces without that cost.

15Sampling and the Tree: Keeping Families Together

At high traffic, you cannot keep every trace. Sampling decides which ones to keep, and it must respect parent-child relationships, or you end up with trees that have missing branches.

Why parents and children must agree

Suppose the gateway decides to keep a trace, but the order service independently decides to drop its part. The saved trace would show a gateway span with a missing branch where the order service should be. The picture lies by leaving things out. To prevent this, the sampling decision is made once, at the root, and carried to every child through the sampled flag inside the trace context. Children that see the flag simply follow it.

The standard tool for this is the parent-based sampler. For a root span, it asks a normal sampler, such as a ratio sampler, to decide. For a child span, it copies the parent’s decision. The result is that a trace is either fully kept or fully dropped, and the tree stays whole.

Everyday Analogy

A photographer is told to keep one photo album out of every ten family reunions. The decision is made for the whole reunion, not for each person. If she kept random pages from every album, no album would make sense. Keeping or dropping an entire trace follows the same sense.

What dropped parents mean for children

Even when a span is not sampled, the SDK still creates a lightweight placeholder that carries the span context. This lets child spans and outgoing calls inherit the same trace ID and “not sampled” flag, so downstream services also stay quiet. Dropped spans cost almost nothing and still keep the family consistent.

SetupResult for the tree
Parent-based sampler everywhereWhole traces are kept or dropped. Trees are complete.
Independent ratio sampler in each serviceTrees have holes, since each service flips its own coin.
Always-on in some services, parent-based in othersSome trees are complete, and some services produce extra roots or fragments.
Tail sampling in a CollectorComplete trees are chosen after the fact, for example all traces with an error.
!
Watch Out

Traces that start partway, because one service restarts the trace instead of continuing it, can double your data and break trees. Make sure each service continues the incoming context rather than starting its own root.

16Security: Trusting the Parent Header

A trace header is just text that anyone can write. Services that accept parent information from strangers need to think about what could go wrong.

Forgery

Fake parent IDs

An outside caller can send any trace ID and parent ID. Your spans may then be attached to a trace that does not belong to your system, or may collide with real IDs and confuse the tree.

Cost

Forced sampling

A caller may set the sampled flag on every request. If you obey it without limits, an attacker can raise your telemetry volume and your bill.

Leakage

Headers sent outward

When your service calls a third party, it may attach your trace headers. These IDs reveal little, but baggage values sent along can reveal much more.

Content

Sensitive span data

A parent and child may both hold URLs, queries, or customer IDs as attributes. Anyone who can view the trace can read them.

Access

Who can view trees

A full tree reveals your architecture, call order, and timing. That is useful to attackers too, so restrict access to tracing tools.

Receiver

Open collector ports

An unprotected collector accepts forged spans with made-up parents. Require authentication and encrypt connections.

Practical protections

  • At the public edge, do not trust incoming trace context. Start a new root span and record the incoming context as a link instead of a parent, or accept it only from known partners.
  • Configure which outgoing destinations receive propagation headers, so internal IDs and baggage do not reach unknown third parties.
  • Cap how much the incoming sampled flag can raise your volume, using rate limits in the Collector.
  • Scrub sensitive attributes centrally, before data is stored.
!
Watch Out

Do not place passwords, tokens, or personal data into baggage. Baggage is copied into every outgoing call that follows the propagation rules, including calls you did not plan for.

17Performance, Reliability, and Monitoring the Tree

Parent-child spans are cheap one at a time, but trees can grow big. A few habits keep them healthy.

The cost of a bigger tree

Every child span costs a little CPU to create, a little memory to hold, and some bandwidth and storage to ship and keep. A trace with 20 spans is cheap. A trace with 20,000 spans, produced by a loop that creates a span per item, is painful to produce, send, store, and read. The SDK also enforces limits on how many events, links, and attributes one span may hold, so a single span cannot grow without bound.

20
spans in a healthy request, as an example
20,000
spans when a loop creates one per item
1,000x
difference in cost between the two

Signs that your tree needs attention

Too deep

Very long chains

Many tiny wrapper spans stacked inside each other add noise and hide the real work. Remove spans that add no information.

Too wide

One parent, hundreds of children

This often means a loop or an N+1 query pattern. Fix the code, or group the work into one span with a count attribute.

Too flat

No nesting at all

Spans that should be children appear as unrelated roots. Context is lost somewhere, so check propagation and activation.

Too many orphans

Missing parents

Spans arrive but their parents never do. Look for dropped exports, full queues, or a service that is not exporting.

Measuring the health of relationships

Treat the quality of your traces as something to monitor. Useful signals include the share of traces that have exactly one root, the number of orphan spans per hour, the average and maximum number of spans per trace, and the share of cross-service calls whose server span has a remote parent. If these numbers worsen after a release, a change probably broke propagation.

i
Tip

After every deployment, open one fresh trace from a key user journey and confirm that it is a single tree with the services you expect. It takes one minute and catches most broken propagation.

18Advantages, Disadvantages, and Trade-offs

The parent-child model is the foundation of distributed tracing, but it is a model, and every model has limits. Here is an honest balance.

Advantages

  • Shows cause and effect, not only timing.
  • Easy to compute self time and find the critical path.
  • Works across languages, vendors, and networks, because it needs only a few IDs.
  • Each service needs to know only its own span and its parent, so it scales well.
  • Powers service maps, error attribution, and latency breakdowns in most tools.
  • Easy to learn, since it matches how people already think about “who called whom”.

Disadvantages

  • One parent per span cannot express many-to-one causes without extra links.
  • Broken context creates orphan spans and split traces that are hard to diagnose.
  • Asynchronous work can make parent and child timing look confusing.
  • Clock differences between hosts can make children seem to start early.
  • Large trees can be costly and hard to read.
  • Quality depends on every service forwarding context correctly.

Decisions to make on purpose

DecisionChoose the first whenChoose the second when
Parent or link for async workThe caller logically waits for or owns the workThe work is related but independent or batched
Continue the incoming trace or start freshThe caller is a trusted internal serviceThe caller is the public internet or a partner you do not control
Many small spans or few big spansYou need precise timing for each stepCost and readability matter more
One long trace or several linked tracesThe whole journey is shortThe journey spans hours, queues, or schedules

19Design Patterns and Anti-patterns

Teams who read traces every day keep arriving at the same advice. Here are the habits that work, and the traps to avoid.

Patterns worth copying

Pattern

One span per meaningful step

Create spans for incoming requests, outgoing calls, database access, and important business steps. Skip trivial helpers.

Pattern

Let the framework do the nesting

Use automatic instrumentation for servers and clients, then add manual spans inside them. Nesting follows naturally.

Pattern

Wrap-and-activate helpers

Use the “start as current span” style helpers so a span is both created and made active, with automatic ending.

Pattern

Links at batch boundaries

When many inputs feed one job, start a fresh trace and link to the inputs.

Pattern

Events for tiny details

For small moments inside a step, such as “cache miss”, add an event to the current span instead of creating a child.

Pattern

Trust boundary at the edge

Treat incoming trace context from the public internet as untrusted and decide deliberately whether to continue or restart it.

Anti-patterns to avoid

ANTI-PATTERN 01Avoid
The Mistake

Creating a span for every iteration of a loop, so one request produces thousands of sibling children under a single parent.

Why It Hurts

The trace becomes huge and unreadable, costs rise sharply, and the one slow item is buried among thousands of near-identical bars.

Do This Instead

Wrap the whole loop in one span, record the item count as an attribute, and add a span only for items that are slow or fail.

ANTI-PATTERN 02Avoid
The Mistake

Creating a span but never making it current, or handing work to another thread without handing over the context.

Why It Hurts

Children cannot find their parent, so they attach to the wrong span or become new roots. Traces turn flat or split apart.

Do This Instead

Use the helpers that activate spans automatically, and use the SDK’s context-wrapping tools for pools and callbacks.

ANTI-PATTERN 03Avoid
The Mistake

Building your own parent links by copying span IDs into custom headers or log fields and trying to reconnect them later.

Why It Hurts

You lose compatibility with standard tools, you must write your own tree builder, and every service must follow your private rules.

Do This Instead

Use the standard propagators and the standard span context. Add links when you need extra connections.

20Best Practices and Common Mistakes

If you remember only this chapter, you will still avoid most of the pain beginners meet when working with span relationships.

1

Name spans for the operation

Use short, stable names such as “POST checkout” or “charge card”. Keep unique values, such as IDs, in attributes and out of names.

2

Use the right span kind

Mark outgoing calls as CLIENT and incoming calls as SERVER. Tools rely on this to draw service maps.

3

Always end your spans

A span that never ends is never exported and leaves its children as orphans. End spans in a finally block or use a helper that does it for you.

4

Record errors on the right span

Set the error status and record the exception on the span where it happened, so the tree points to the true source.

5

Forward headers at every hop

Check proxies, gateways, and queues. Every hop must pass the trace headers along unchanged.

6

Use one propagation format everywhere

Standardize on W3C Trace Context unless a legacy system requires another one.

7

Test a real trace after each release

Open a fresh trace of a key journey and check that it forms one tree.

Common mistakes and their fixes

MistakeWhat you will seeFix
Span created but not made currentFlat trace with unrelated siblingsUse a start-as-current helper or activate the span
Span never endedMissing parent, orphaned childrenEnd spans in all code paths, including errors
Context not passed to a new threadNew root span in the middle of a requestWrap the task so it carries the context
Proxy removes trace headersTrace splits at that proxyAllow and forward the trace headers
Different propagation formatsDownstream service starts a new traceConfigure both sides with the same propagators
Independent sampling per serviceTrees with missing branchesWrap the sampler in a parent-based sampler
Wrong span kindMissing arrows on the service mapUse CLIENT and SERVER for network calls

21Real-World and Industry Examples

First, a made-up story that shows the idea at work. Then a look at how real tools and platforms use the parent-child model.

An illustrative story: the ride that took too long to confirm

Imagine a ride-booking app. A rider taps “Confirm”, and the confirmation takes six seconds. An engineer opens one slow trace. The root span is the API gateway’s handling of the request, and under it sit the booking service, the pricing service, and the driver-matching service. The waterfall shows that booking took 5.8 seconds, and almost all of it sits inside one child span called “find nearby drivers”, which called a location database 140 times in a row. Each call was quick, but they ran one after another. The fix was to fetch all locations in one query, and the confirmation dropped to under a second. Without the parent-child tree, the team would have seen only a slow booking service. (This is a made-up example to show the workflow, not a report from a real company.)

Dapper, Zipkin, and Jaeger: the waterfall view

Google’s Dapper paper introduced the tree of spans for tracing large distributed systems. Zipkin and Jaeger, two widely known open-source tracing tools, present traces as parent-child waterfalls and build service dependency graphs from the same relationships. Jaeger began at Uber and later became a graduated CNCF project, and it now works with OpenTelemetry data.

Proxies and service meshes

Edge proxies such as Envoy can create the first span of a trace at the front door and pass the context to the services behind them. This is a common way to get a clean root span, and to add spans for network hops that application code never sees.

Cloud platforms

Major cloud providers and monitoring vendors accept OpenTelemetry trace data, and the W3C Trace Context header has been adopted widely. This is why a trace can start in an application running on one platform and continue through managed services on another, as long as the header is forwarded.

Kubernetes

Kubernetes control-plane components can emit spans in OpenTelemetry format. The parent-child model then lets operators see how a single cluster request fans out into several internal steps, using the same tooling they use for applications.

22Frequently Asked Questions

Quick answers to the questions people ask most often about span relationships.

Q1Can a span have more than one parent?
No. Each span has at most one parent. If a span is related to other spans, for example in a batch, you add links to represent those extra connections.
Q2What is the difference between a parent and a link?
A parent means “this span was started by that span”. A link means “this span is related to that span”. Parents build the tree. Links add connections that do not fit in the tree.
Q3How does a service in another language know who the parent is?
Through the trace header, usually traceparent. The caller writes its span context into the header, and the receiver reads it. Both follow the same public format, so languages do not matter.
Q4What is a root span?
It is a span without a parent, the first span in a trace. It often represents the arrival of a request or the start of a job.
Q5Does a parent always last longer than its children?
Usually, but not always. Asynchronous or background children can finish after their parent, and clock differences between machines can also make the timings look uneven.
Q6Why does my trace look flat?
Most likely the parent span was not made current, or context was lost when work moved to another thread or task. Use helpers that activate spans and check how your language carries context.
Q7Why is part of my trace missing?
Common causes are a proxy dropping the trace header, an uninstrumented service in the middle, mismatched header formats, or separate sampling decisions in each service.
Q8What is an orphan span?
An orphan is a span whose parent span has not arrived at the backend. Either the parent is late, or it was lost or never exported.
Q9Should every function be its own child span?
No. Create spans for meaningful steps such as requests, outgoing calls, database queries, and important business operations. Use events for small details inside a span.
Q10How do parent and child relate to sampling?
The decision is made at the root and passed to children through the sampled flag. A parent-based sampler makes children follow it, so traces are kept or dropped as a whole.
Q11Should I trust a trace header from outside my company?
Not blindly. Outside callers can send any IDs and flags. Many teams start a fresh root at the public edge and keep the incoming context only as a link.

23Summary and Key Takeaways

You started with a slow pizza and a vague complaint. You now have the tool that turns vague complaints into precise answers.

Key Takeaways

  • A parent span started the work of a child span. Together, parents and children form a tree for one trace, with a single root at the top.
  • Three IDs store the whole relationship. The trace ID is shared, the span ID is unique, and the parent span ID points to the span that started this one.
  • The SDK finds the parent from context. It uses an explicit context if given, otherwise the active span, otherwise it creates a new root. Creating a span does not automatically make it active.
  • Propagation carries parents across services. The traceparent header holds the caller’s span as the remote parent, so the tree survives network hops and language changes.
  • Use links for connections that are not parenthood. Batches, fan-in, and long delays are better shown with links than with forced parents.
  • Read the tree for time and cause. Compare parent and child durations, find self time, and follow the critical path. Remember that clocks and async work can make timing look odd.
  • Protect and monitor the tree. Use parent-based sampling, treat outside trace headers as untrusted, and watch for flat traces, orphans, and oversized trees.