What Is a Span Context in OpenTelemetry?
A friendly, ground-up guide to the small identity card that travels with every span: what it holds, why it exists, how it crosses the network, and how to use it without causing trouble.
When you travel abroad, you carry a passport. It is small, it fits in your pocket, and it cannot be changed after it is printed. It does not describe your whole life. It does not list your hobbies or what you ate for lunch. It simply proves who you are, so that every officer at every border can recognise you and connect your journey together.
In OpenTelemetry, every piece of work that is tracked, called a span, has something very similar. It is called the span context. It is the small, unchangeable identity card that says “I belong to this journey, and I am this exact step.” Without it, a request that passes through ten different services would look like ten unrelated events. In this guide, we will learn what a span context is, what is inside it, and why so much of OpenTelemetry depends on it.
1The Problem: How Do We Know Which Steps Belong Together?
Before learning the idea, let us feel the problem it solves.
One click, many programs
Imagine you tap “Place order” in a food delivery app. Behind that single tap, many separate programs may do work: a gateway receives the request, an order service records it, a payment service charges your card, and a notification service sends you a message. These programs may run on different machines, in different countries, and may be written in different languages.
Each program can easily record its own timing. But how do we know that the payment record at 7:02 pm belongs to the same order as the gateway record at 7:02 pm? Thousands of other customers are ordering at the same moment. Matching records by time alone would be like trying to find your luggage at a huge airport by guessing.
The need for a shared identity
What we need is a label that is created at the start of the journey and carried along to every step. Every step must stamp that same label on its own records. We also need each step to have its own identity, so we can tell step three from step four. And we need a few small extra facts, such as whether the journey is being recorded. Together, these facts make up the span context.
Think of a courier parcel. It has one tracking number for the whole delivery, and every warehouse scan adds a new line. The tracking number is shared by everyone. The scan line is unique to each stop. A span context is like holding both of these together: the shared tracking number and the identity of the current stop.
2So, What Is a Span Context?
First the short answer, then the pieces one by one.
Let us first recall two words. A trace is the complete journey of one request through your system. A span is one timed step inside that journey, such as “charge the card.” A span has many details: a name, a start time, an end time, attributes, events and a status.
A span context is the small part of a span that identifies it and lets it be connected to other spans. It is the part that can safely travel from one program to another. It is immutable, which means it cannot be changed once it is created. It is also serializable, which means it can be turned into a short piece of text and sent across the network.
What is inside a span context?
A span context has a small number of fields. Each one has a clear job.
flowchart TD
SPAN["Span"] --> DET["Details for this process only"]
SPAN --> SC["Span Context"]
DET --> D1["Name"]
DET --> D2["Start and end time"]
DET --> D3["Attributes and events"]
DET --> D4["Status"]
SC --> F1["Trace ID"]
SC --> F2["Span ID"]
SC --> F3["Trace Flags"]
SC --> F4["Trace State"]
SC --> F5["Is Remote"]
Trace ID
A 16-byte value, shown as 32 hexadecimal characters. Every span in the same trace has the same one. It is the tracking number of the whole journey.
Span ID
An 8-byte value, shown as 16 hexadecimal characters. It identifies one single span inside the trace, like one line on the tracking page.
Trace Flags
One byte of small on and off switches. The best known one is the sampled flag, which says whether this trace is being recorded.
Trace State
An optional list of key and value pairs that vendors can use to carry their own hints alongside the standard fields.
Is Remote
A yes or no value saying whether this context came from another process over the network, or was created locally.
Think of a school ID card. It has the school’s name (shared by all students, like the trace ID), the student’s own number (unique, like the span ID), a small colour band for special status (like the flags), and optionally a sticker from the sports club (like the trace state). The card says nothing about the student’s marks. Marks are details, not identity.
3Core Concepts: The Fields in More Detail
Each field is small, but each has a careful design behind it.
Trace ID: the shared journey number
The trace ID is a random 128-bit number. To get a feel for how big that is, there are about 340 undecillion possible values, which is a number with 39 digits. Because the values are chosen at random, the chance that two different journeys ever receive the same trace ID is so small that, for real systems, you can safely ignore it. No central office is needed to hand out IDs. Every program can create one on its own.
A trace ID made only of zeros is not allowed. It is treated as invalid, a bit like an empty passport.
Span ID: the step number
The span ID is a random 64-bit number. It only needs to be unique inside its own trace, because it is always used together with the trace ID. As with the trace ID, all zeros means invalid. When a new span is created, it receives a fresh span ID. When the next service calls onward, the caller’s span ID becomes the parent that the next span points to.
Trace flags: the small switches
Trace flags are stored in a single byte. The most important switch is the sampled flag. When it is on, it says “this trace is being kept, so please record your part too.” When it is off, it says “this trace is not being kept, so you may skip detailed recording.” This one bit lets every service in a journey agree on a decision made at the start, without needing to talk to each other.
Newer versions of the W3C standard also define a flag that says the trace ID was generated randomly in a way that other systems can trust for sampling decisions. Support for this varies, so check what your tools support.
Trace state: vendor notes
The trace state is a short, ordered list of key and value pairs. It exists so that different vendors and systems can pass their own small pieces of information along the journey without breaking the standard fields. For example, a sampling system might record the probability it used. The list is limited in size, and systems must pass along entries they do not understand rather than throwing them away.
Is remote: where did this come from?
This field is not sent over the network. It is set by the receiver. When a service extracts a span context from an incoming request, it marks it as remote. When the context was created inside the same program, it is not remote. This helps systems know whether a parent span lives in this process or somewhere else, which is useful for deciding things like the kind of span to create.
A span context is considered valid only if both the trace ID and the span ID are non-zero. Many libraries have a simple “is valid” check, and it is wise to use it before trusting a context that came from outside.
4Span versus Span Context: Clearing Up the Confusion
Beginners often use these two words as if they meant the same thing. They do not.
| Question | Span | Span Context |
|---|---|---|
| What is it? | One timed piece of work with all its details | The identity of that span |
| Can it change? | Yes, while the span is open you can add attributes, events and status | No, it is immutable |
| Does it travel across the network? | No, a span stays in the process that created it and is exported later | Yes, it is carried in request headers or message headers |
| How big is it? | Can be large, with many attributes and events | Tiny, a few dozen bytes |
| Who needs it? | Backends that show traces | Other services, log systems, metric systems and the SDK itself |
A patient’s full medical file is the span. It is thick and contains private details, and it stays in the hospital. The patient wristband is the span context. It is small, it cannot be edited, and it goes everywhere with the patient so that every department can find the right file.
This is a clever design. Spans can be large and rich, but only the tiny identity card needs to cross network borders. That keeps request headers small and lets each service keep its detailed story to itself, sending it later to the backend.
5Internal Working: Birth of a Span Context
Where do these IDs come from, and what happens when a child span is created?
The root span: starting a new journey
When a request arrives and there is no incoming context, the SDK creates a root span. It asks an ID generator for a brand new trace ID and a brand new span ID. It also asks the sampler whether to record this trace, and sets the sampled flag based on the answer.
Child spans: continuing the journey
When code starts a new span while another span is active, the SDK looks at the current context to find the parent. The child then follows a simple set of rules.
flowchart TD
R["Root span: Place order"] -->|"Same Trace ID. New Span ID."| C1["Child span: Check stock"]
R -->|"Same Trace ID. New Span ID."| C2["Child span: Charge card"]
C2 -->|"Same Trace ID. New Span ID."| G1["Grandchild span: Call card provider"]
Copy the Trace ID
The child uses the same trace ID as its parent. This is what ties the whole tree together.
Generate a New Span ID
The child gets its own fresh span ID. The parent’s span ID is remembered separately as the parent link, but it is not part of the child’s own span context.
Inherit Flags and State
The child normally keeps the parent’s trace flags and trace state. A well-behaved sampler follows the parent’s sampled decision.
Freeze the Context
Once built, the span context never changes. If something needs to change, such as the trace state, a new span context is created instead.
Notice that a span’s parent is not stored inside its span context. The parent relationship is kept as a separate piece of information on the span. The span context only describes the span itself.
6Data Flow: Crossing the Network with Propagation
This is the reason the span context exists.
A span context only lives inside one program’s memory. To keep a trace whole across the network, it must be written into the outgoing request and read back by the receiver. Writing it is called inject. Reading it is called extract. The tool that does this work is a propagator.
It is a relay race. The baton is the span context. The first runner must hand over the baton, and the next runner must receive it. If the handover fails, the next runner starts a brand new race, and the team loses track of the original one.
sequenceDiagram
participant G as API Gateway
participant O as Order Service
participant Q as Message Queue
participant E as Email Worker
G->>G: Create root span and span context
G->>O: Inject span context into request header
O->>O: Extract span context and mark it remote
O->>O: Create child span with same trace ID and new span ID
O->>Q: Inject span context into message headers
Q->>E: Deliver message with headers
E->>E: Extract span context and create consumer span
The standard format: W3C Trace Context
By default, OpenTelemetry uses the W3C Trace Context standard. It defines a request header named traceparent. It has four parts separated by dashes: a version, the trace ID, the span ID of the caller, and the flags. An example value looks like this:
| Part | Example Value | Meaning |
|---|---|---|
| Version | 00 | The version of the format |
| Trace ID | 0af7651916cd43dd8448eb211c80319c | 32 hexadecimal characters, the shared journey number |
| Parent Span ID | b7ad6b7169203331 | 16 hexadecimal characters, the caller’s span |
| Flags | 01 | The sampled flag is switched on |
A second header, tracestate, carries the trace state. Because both headers follow an open standard, a Java service can call a Go service, which calls a Python service, and the trace stays whole. Other formats are also supported through propagator plugins, such as B3 from the Zipkin world, the Jaeger format, and the AWS X-Ray header. These help during migrations.
Messages, not just web calls
Span contexts do not only travel in web requests. For message queues and event streams, the context is placed in the message’s own headers or properties. The consumer extracts it and creates a span that either continues the same trace or, for batch processing, links back to the original. This is how a trace can follow an order from the web page, through a queue, all the way to the email worker that runs minutes later.
7The Sampled Flag: One Bit, Big Consequences
The flag that lets hundreds of services agree without a meeting.
Recording every trace in a large system is very expensive. Sampling means keeping only some traces. The key challenge is agreement. If the gateway decides to keep a trace but the payment service decides to drop its part, the trace will have holes. The sampled flag solves this by carrying the first decision to everyone downstream.
flowchart TD
A["Request arrives"] --> B{"Valid incoming span context?"}
B -->|No| C["Create root span and let the sampler decide"]
B -->|Yes| D{"Sampled flag on?"}
D -->|Yes| E["Record this span and pass flag on"]
D -->|No| F["Do not record details but still pass context on"]
C --> G["Set sampled flag on the new span context"]
G --> H["Call next service with the flag"]
E --> H
F --> H
The usual sampler used in production is called a parent-based sampler. Its rule is simple: if there is a parent, follow the parent’s decision. If there is no parent, make a fresh decision, for example by keeping ten percent of traces.
Recording versus sampled: two different ideas
These two words sound similar, but they are separate. A span that is recording collects details like attributes and events inside the program. A span that is sampled carries the flag that says the trace will be kept and exported. In most setups, the two go together, but they can differ. A program can record a span locally for its own use, for example to compute local metrics, without marking the trace as sampled for others.
Even when a trace is not sampled, the span context should still be passed to the next service. If you drop the context, the next service thinks it is the start of a brand new journey and may decide to sample on its own, which creates confusing partial traces.
Tail sampling and the flag
Some systems decide after the trace is finished, called tail sampling. In that case, spans are usually recorded and exported to a central processing point, which then decides what to keep. The sampled flag is less meaningful there, because the real decision is made later. This is why it is important to know which style your own system uses.
8The No-Op Surprise: Propagation Without an SDK
A clever detail in the design that protects your traces.
The OpenTelemetry API is separate from the SDK. If a program uses only the API and no SDK is installed, the tracing calls do nothing, which is called no-op. You might expect that this would break trace connections, since nothing is recorded. But the design is smarter than that.
When the API has no SDK behind it, it returns a non-recording span that simply wraps the incoming span context. This means that even a service with no real tracing setup will still pass the trace identity along to the services it calls. A trace can therefore survive an uninstrumented hop in the middle of a journey.
Think of a person at the middle of a relay race who has no stopwatch. They do not time anything, but they still carry the baton from one runner to the next. The race remains connected even though that runner kept no records.
Production Example: A Library in the Middle
A company has an old internal service that nobody has instrumented. Because it uses a framework with API-only support and a propagator, it still forwards the trace headers. Engineers see a gap in the trace where the service should be, but the spans before and after it still belong to the same trace. Without this behaviour, the trace would split into two unrelated pieces.
9Where Else Is the Span Context Used?
Because it is small and standard, many parts of OpenTelemetry borrow it.
Log Correlation
When a log record is written while a span is active, the SDK can stamp the log with the trace ID and span ID. Engineers can then click from a trace straight to its logs.
Exemplars
A metric point, such as a slow response time, can carry an example trace ID and span ID, called an exemplar, so you can jump from a graph spike to a real slow trace.
Span Links
A span can point to other spans that are related but not parent or child, using their span contexts. A batch job can link to every message it processes.
Remote Parents
When creating a span from an incoming request, the extracted span context becomes the parent, even though that parent lives in another program.
Sampler Input
Samplers receive the parent span context so they can follow or override the earlier decision.
Support Tickets
Some products show the trace ID in an error page, so a customer can quote it to support, who can then find the exact trace.
Production Example: From Graph to Root Cause
An engineer sees a spike in checkout latency on a dashboard. A point on the graph carries an exemplar. One click opens the exact slow trace. From that trace, another click shows the log lines written during the slow database span. Three kinds of data are joined together through nothing more than two small IDs from the span context.
10Span Context versus Baggage versus Attributes
Three ways to carry information, each for a different purpose.
| Feature | Span Context | Baggage | Span Attributes |
|---|---|---|---|
| Purpose | Identify the span and connect the trace | Carry custom notes downstream | Describe one span in detail |
| Travels across services? | Yes | Yes | No, stays with the span |
| Who defines the fields? | The standard, fixed fields | You decide the keys and values | You decide, guided by conventions |
| Can be changed? | No, immutable | New values can be added by creating new baggage | Yes, while the span is open |
| Size | Tiny and fixed | Can grow, so keep small | Can be large |
| Everyday picture | Passport | Sticky note on the parcel | Notes in a diary |
A common beginner mistake is trying to put business data, such as a customer ID, into the span context or the trace state. The span context is for identity. If downstream services need business hints, use baggage carefully. If you only need the detail for searching later, use attributes on the span.
11Advantages, Disadvantages and Trade-offs
A design this small has big strengths, and a few honest limits.
Advantages
- Tiny: adds only a few dozen bytes to each request
- Standard: works across languages, vendors and platforms
- No central coordination: any program can create IDs on its own
- Immutable: safe to share between threads without locks
- Reusable: the same IDs link traces, logs, metrics and links
- Resilient: survives hops through uninstrumented components
Disadvantages
- Depends on every hop passing it on: one broken hop splits the trace
- Trust problem: incoming contexts can be forged or abused
- Limited content: not meant for business data
- Format mixing: several header formats can cause confusion in migrations
- Randomness assumptions: some sampling methods rely on the quality of random IDs
Key trade-offs
| Decision | Option A | Option B |
|---|---|---|
| Trust incoming context at the public edge | Continue the caller’s trace for full end-to-end visibility | Start a fresh trace and link to the caller for safety |
| Trace state usage | Use it for useful vendor hints | Keep it empty for minimal header size and simplicity |
| Header formats | Use only W3C for simplicity | Support several formats to bridge old systems |
| Messages in queues | Continue the same trace for a single story | Start new traces and use links, which suits batch processing better |
12Security Considerations
The span context is public information in transit, and it comes from the outside world.
IDs Are Not Passwords
Trace IDs and span IDs travel in plain headers. Never use them as login tokens, session keys or proof of identity.
No Personal Data Inside
Do not build IDs from user names, emails or phone numbers, and do not put such values in trace state. IDs should be random.
Treat Incoming Context as Untrusted
At a public gateway, an outsider can send any trace header. Decide whether to accept it, restart the trace, or link to it.
Forced Sampling
An attacker could set the sampled flag on many requests to make you record and store more than planned. Use rate limits or ignore the flag at public edges.
Limit Trace State
Oversized or malformed headers can waste memory. Libraries limit them, but you should also limit them at your proxies.
Outgoing Calls to Third Parties
If you inject headers into calls to outside companies, they will see your trace and span IDs. This is usually harmless, but know that it happens, and avoid it if it reveals internal structure you want private.
Never assume that an incoming trace ID is unique or correct. A buggy or malicious caller may reuse the same trace ID for many requests, which would make unrelated work appear as one giant trace.
13Monitoring, Logging and Metrics: Spotting Broken Context
When span contexts get lost, the symptoms are easy to spot if you know where to look.
- Many tiny traces: if your traces mostly contain just one or two spans, context is probably being dropped between services.
- Orphan spans: spans that point to a parent that never shows up suggest that the parent was dropped, not sampled, or lost in transit.
- Roots in the wrong place: if root spans appear deep inside your system and not at the edge, an upstream hop is failing to pass the context on.
- Invalid context counts: count how often incoming headers are missing or malformed at your gateways.
- Log correlation rate: measure what share of log lines carry a trace ID. A low share points to missing logging integration.
- Header size: watch for unusually large trace state or baggage, which can cause rejected requests at proxies.
- Sampling consistency: check that the sampled share matches your plan. Large differences may signal forced sampling or broken parent-based rules.
Production Example: The Silent Proxy
A team added a new reverse proxy to improve security. Soon, their traces split in half at that point. The proxy’s strict rules were removing unknown headers, including traceparent. A dashboard showing “share of traces with more than three spans” fell sharply, and a quick review of the proxy settings fixed it by allowing the trace headers through.
14Design Patterns and Anti-Patterns
Habits to copy and traps to avoid.
Good design patterns
Value Object
A span context is a small immutable value, which is a classic safe design. Create a new one rather than editing it.
Carrier and Propagator
Hide the header details behind a propagator so your code never touches raw header strings.
Middleware at the Edge
Extract context once in shared middleware for each server, and inject once in each client library, instead of in every handler.
Context Capture for Async Work
Capture the current context before handing work to another thread or task, and restore it there.
Edge Policy
Define one clear rule at public gateways for accepting, restarting or linking incoming traces.
Log Enrichment
Add trace and span IDs to every log line automatically through the logging integration.
Anti-patterns
The Mistake
Making your own trace IDs from business values, such as an order number.
Why It Hurts
Such IDs may repeat, may be guessable, can leak private data and may break sampling methods that expect randomness.
Better Approach
Let the SDK generate random IDs. Store the order number as an attribute on the span, so you can search for it.
The Mistake
Dropping the span context when a trace is not sampled.
Why It Hurts
Downstream services will start new traces and may sample them, giving you partial and confusing data.
Better Approach
Always pass the context along, even when not recording. The flag tells everyone what to do.
The Mistake
Using the trace ID as a security token or as a stand-in for a user session.
Why It Hurts
The ID is visible in headers, logs and error pages. Anyone who sees it could reuse it, and it proves nothing about who they are.
Better Approach
Use real authentication and sessions. Treat trace IDs as labels for debugging only.
The Mistake
Stuffing large amounts of data into trace state.
Why It Hurts
It grows every request header, may be truncated or rejected, and is carried through every hop whether needed or not.
Better Approach
Keep trace state tiny and rare. Use baggage for small hints, and attributes for details.
15Best Practices and Common Mistakes
A practical checklist for keeping your traces whole.
Best practices
- Use the standard format. Keep W3C Trace Context as your default, and add other formats only where old systems need them.
- Check every hop. Gateways, proxies, load balancers and service meshes must allow trace headers through.
- Allow headers in browsers. When a web page calls your API from another domain, make sure the cross-origin rules allow the trace headers.
- Propagate through queues. Always place the context in message headers, and extract it in consumers.
- Capture context for async work. Before starting background tasks, make sure the current context is passed in.
- Check validity. Test whether an incoming context is valid before using it as a parent.
- Use parent-based sampling. Let each service follow the first decision unless you have a strong reason not to.
- Decide a public edge policy. Choose whether to continue, restart or link incoming traces at internet-facing points.
- Add IDs to logs. Let your logging setup add trace and span IDs automatically.
- Test propagation. Add a simple end-to-end test that calls through all your key services and checks that one trace ID appears throughout.
Common mistakes beginners make
- Thinking the span context holds the span’s attributes or timing. It holds identity only.
- Believing the parent span ID is part of the span’s own context.
- Forgetting that the context must be injected into outgoing calls to services that are not instrumented automatically.
- Losing the context in thread pools and async code.
- Mixing up “recording” with “sampled.”
- Assuming trace IDs are always unique when they came from an untrusted caller.
- Putting sensitive information into trace state or baggage.
16Real-World and Industry Examples
How span context ideas show up in systems you may already know.
Zipkin and the B3 Headers
Zipkin, one of the earliest open-source tracing systems, popularised passing trace and span IDs in request headers, known as B3. Many companies still have services using it, so OpenTelemetry offers a B3 propagator. This lets a company move to the W3C format gradually, service by service.
Jaeger
Jaeger, which began at Uber and is now a CNCF project, has its own header format. OpenTelemetry supports it through a propagator, and Jaeger’s maintainers now recommend OpenTelemetry for instrumentation. Jaeger then stores and shows traces built from these identities.
Amazon Web Services
AWS X-Ray uses its own trace header format. The AWS Distro for OpenTelemetry and the X-Ray propagator let applications read and write it, so a trace can pass through AWS-managed components and your own OpenTelemetry-instrumented code without breaking.
Envoy, gRPC and Service Meshes
Proxies and frameworks such as Envoy and gRPC can read and write trace headers. When they follow the W3C standard, the spans they create join the same trace as your application spans, giving you a view of network hops without any extra code in your services.
Messaging Systems
Teams that use event streams, such as Kafka, carry the span context in message headers. The consumer reads it and creates a span connected to the producer, so that a single trace can follow an order from checkout all the way through background workers.
A typical journey for a growing company
Turn On Default Propagation
Enable W3C Trace Context in all services so that web calls carry the same trace ID.
Fix the Gaps
Find hops that drop headers, such as proxies and older libraries, and configure them to pass the headers.
Extend to Messages and Jobs
Carry context through queues, scheduled jobs and background tasks.
Connect Logs and Metrics
Add trace IDs to logs and exemplars to metrics, joining all signals.
Set Edge and Sampling Policies
Decide how to treat outside callers, and use parent-based sampling to keep costs under control.
17Frequently Asked Questions
Quick answers to questions beginners ask most.
18Summary and Key Takeaways
If you remember only a few things, remember these.
Key Takeaways
- A span context is an identity card. It is a tiny, unchangeable bundle holding the trace ID, span ID, trace flags, trace state and a remote marker.
- The span is the story, the span context is the identity. Only the small identity needs to travel between services, while the detailed span stays where it was created.
- One trace ID joins everything. Every span in a journey shares the trace ID, and each has its own span ID, which builds the tree you see in a trace viewer.
- Propagation keeps the chain alive. Inject the context into outgoing calls and extract it on arrival, using the W3C traceparent header or message headers for queues.
- The sampled flag spreads one decision. It lets all services agree on whether to record a trace, so always pass the context on, even when not sampling.
- The same IDs link every signal. Logs, metric exemplars and span links all reuse the trace and span IDs to connect different kinds of data.
- Treat incoming context with care. IDs are not secrets and not proof of identity, so protect public edges, keep data out of them, and watch for broken or forged contexts.