A span is the basic unit of a distributed trace: one named, timed operation such as an incoming HTTP request, a database query or publishing a message. Each span carries trace and span IDs, a parent ID, timestamps, a kind, attributes, events, links and a status, showing where time was spent and errors occurred.
What is a span?
A span represents a single unit of work inside a request: a service handling GET /orders/{id}, an outgoing call to a payment API, a SQL query, a message consumed from Kafka. Spans that share a trace ID form a trace, and parent-child relationships between spans turn that trace into a tree showing how a request travelled through the system and how long each step took. This is the foundation of distributed tracing.
The first span of a trace, with no parent, is the root span. Spans are created by instrumentation, either automatically by libraries and agents or manually in application code, and are linked across process boundaries through context propagation.
Span fields in the OpenTelemetry specification
| Field | Meaning |
|---|---|
| Name | Low-cardinality description of the operation, e.g. GET /orders/{id} or SELECT orders |
| Span context | Trace ID (16 bytes, 32 hex characters), span ID (8 bytes, 16 hex characters), trace flags (including the sampled bit) and trace state |
| Parent span ID | Span ID of the parent; empty for a root span |
| Span kind | SERVER, CLIENT, PRODUCER, CONSUMER or INTERNAL |
| Start and end time | Timestamps from which the duration is derived |
| Attributes | Key-value pairs such as http.request.method, http.response.status_code, db.system |
| Events | Timestamped annotations within the span, e.g. an exception with type, message and stack trace |
| Links | References to spans in other traces, e.g. for batch processing or fan-in from a queue |
| Status | Unset, Ok or Error, with an optional description for errors |
Each span is also associated with a resource (the entity that produced it, identified by attributes like service.name and k8s.pod.name) and an instrumentation scope (the library that created it).
Span kinds and how they connect
Span kind tells the backend how a span relates to others. A CLIENT span in the checkout service and the matching SERVER span in the payment service describe the same call from both sides; the gap between them is network and queueing time. PRODUCER and CONSUMER describe asynchronous messaging, where the consumer may run long after the producer finished. INTERNAL covers work that stays inside one process. Correct span kinds are what allow tools to draw service maps and compute per-service latency accurately.
Reading span durations
A span's duration includes the time of all its children. To find the real bottleneck, look at self time: the part of a span's duration not covered by any child span. A 900 ms server span with an 850 ms database child points at the query; a 900 ms span whose children add up to 50 ms points at work inside the service itself, such as CPU-bound code, lock contention or garbage collection, where continuous profiling takes over. Children running in parallel can also overlap, so their durations do not simply add up to the parent.
Example: a simplified span
{
"name": "POST /orders",
"kind": "SERVER",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "",
"startTime": "2026-10-15T09:12:03.120Z",
"endTime": "2026-10-15T09:12:03.512Z",
"attributes": {
"http.request.method": "POST",
"http.route": "/orders",
"http.response.status_code": 500
},
"events": [{"name": "exception",
"attributes": {"exception.type": "TimeoutError"}}],
"status": {"code": "ERROR"}
}
This illustrative root span lasted 392 ms and failed. Its child spans, such as a database query and a call to an inventory service, would show which step consumed the time.
Best practices
- Keep span names low-cardinality. Use route templates; put IDs in attributes, not in the name.
- Follow semantic conventions for attribute names so queries work across languages and libraries.
- Set status deliberately. By convention a server span for an HTTP 5xx is an error, while a 4xx is usually the caller's problem and leaves the server span unset. Instrumentation libraries leave successful spans
Unset;Okis reserved for explicit confirmation by application code. - Record exceptions as events so stack traces stay attached to the operation that failed.
- Add business context sparingly: attributes such as tenant tier or feature flag make traces far more useful, but avoid personal data.
Common mistakes
- Creating a span for every function call, which adds overhead and noise without insight.
- Forgetting to end spans, which leaks memory and produces misleading durations.
- Broken context between services, resulting in many disconnected root spans instead of one trace.
- Using links where a parent relationship is correct, or the other way round.
How Obseria helps
Obseria ingests spans via OTLP from any OpenTelemetry SDK or Collector and stores them in Obseria Lake without pre-indexing, so you can filter and aggregate on any attribute. Spans are linked to logs, metrics and profiles of the same service and time window, and Obseria TopoGraph builds service maps from span kinds and parent relationships. Learn more about OpenTelemetry and how spans feed application performance monitoring.