Distributed tracing and OpenTelemetry
Context crosses a process only when the sender injects it and the receiver extracts it and starts its span there; queues too.
Predict first
Before you read: predict
ExamHall's NestJS API serves POST /attempts/a-77/submit. Its OpenTelemetry SDK continues the browser's trace from the traceparent header and wraps the handler in a server span. The handler calls queue.add('grade-exam', { attemptId: 'a-77' }). A separate worker process later takes the job and runs tracer.startActiveSpan('grade-exam', (span) => { … }). Both use the SDK's default sampler and the W3C propagator.
In the export, how does the worker's grade-exam span relate to the API's server span?
The 30-second version
A trace is one trace-id shared by every span of one request. To cross a process, the sender injects its current context into the carrier (HTTP headers, a job's data); the receiver extracts it and starts its span in it, or begins a new trace. The root's sampler decides once; the sampled flag carries that decision downstream.
Context crosses a process only when the sender injects it and the receiver extracts it and starts its span there; queues too.
How it works
Step 1 of 14
In the kitchen
- the job number printed on the traveller, copied onto every station's line → the trace-id (step 1)
Analogy: A machine shop's job traveller (precision engineering)
A batch of shafts moves through a machine shop with a job traveller: a sheet each station fills in before handing the batch on.
| In the machine shop | In tracing |
|---|---|
| the job number printed on the traveller, copied onto every station's line | the trace-id (step 1) |
| each line names the station that handed the batch over | the parent-id (step 4) |
| the receiving station reads the traveller before starting its own line | extract, then start the span in that context (step 5) |
| the "full inspection" box, ticked or not at the first station and honoured by every later station | the sampled flag, decided at the root (steps 8–10) |
| the crate sent overnight to a heat-treater keeps the job number only if the traveller goes inside it and is read at her bench | a queue hop (step 7) |
| the quality office collects every station's sheets, thins them and sends copies on | the Collector (step 12) |
Breaks down when: a traveller is one sheet that travels with the batch, but each service exports its own spans, and the backend joins them by trace-id afterwards. The inspection box is ticked by a person who looks at the batch; a ratio sampler reads only the trace-id.
Contrasting case: a station that writes the job number on a sticky note and keeps it on its own bench has recorded the number but passed nothing on. Writing a trace-id into a log line is that note: it does not continue the trace.
In real terms: inject serialises the current span's ids and flags into a carrier; extract parses them back into a context; the next span's parent is whatever that context holds.
Examples, run for real
4.1 ExamHall EXH-2410: the submit trace stopped at the queue
After a slow grading night, the submit trace showed the API's spans only. The API already stored traceparent in the grade-exam job's data; the worker read job.data.attemptId and nothing else:
4.1 job data stored: attemptId, trace · API and worker on the SDK's default sampler
4.1 before MR !2710 (worker reads job.data.attemptId only): traces 2 · grade-exam parent: none · trace ids differ
4.1 after MR !2710 (worker extracts job.data.trace): traces 1 · grade-exam parent: POST /attempts/:id/submit · trace ids equal
Storing the header was half the hop: the worker never extracted it, so grade-exam began a new trace (step 7). MR !2710 adds propagation.extract(ROOT_CONTEXT, job.data.trace) and starts the span in that context (step 5); grading now sits under the submit request.
4.2 Ticketing: a reseller's headers
A ticket reseller's gateway forwards three kinds of traceparent to the box-office API, which extracts each one and starts a server span:
4.2 valid 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 -> joins trace 4bf92f35…, parent 00f067aa0ba902b7
4.2 uppercase hex 00-4BF92F3577B34DA6A3CE929D0E0E4736-00F067AA0BA902B7-01 -> new trace 6dca1642…, parent none
4.2 all-zero trace-id 00-00000000000000000000000000000000-00f067aa0ba902b7-01 -> new trace 80ec5cc9…, parent none
Only the valid header joins the reseller's trace. The uppercase one and the all-zero trace-id are invalid, so the API starts new traces (steps 3 and 6). Ask the reseller to send lowercase, spec-shaped ids.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
A ticket shop's checkout service receives POST /checkout with the header traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01. No HTTP instrumentation is registered. Its handler, written with the OpenTelemetry API and the W3C propagator, calls the seat service, and its server span gets the id 5fb397be34d26b51:
What traceparent does the seat service receive?
const ctx = propagation.extract(ROOT_CONTEXT, req.headers);const span = tracer.startSpan('POST /checkout', { kind: SpanKind.SERVER }, ctx);await context.with(trace.setSpan(ctx, span), async () => { const headers = {}; propagation.inject(context.active(), headers); await seatService.post('/holds', headers);});span.end();Question 2 of 6
ExamHall's Next.js server calls the grading API over HTTP, inside its own span, and injects its context into the request headers. The grading API's request handler, written with the OpenTelemetry API, begins:
In the trace backend, every grade.submit span is the root of a trace of its own.
Which one change makes grade.submit a child of the Next.js server's span, in the same trace?
const incoming = propagation.extract(context.active(), req.headers);logger.info('grade request', { traceId: trace.getSpanContext(incoming)?.traceId });const span = tracer.startSpan('grade.submit');Question 3 of 6
A video platform's edge gateway keeps 1 in 10 traces, and it sends request R on with traceparent flags 00. The playback API, on the OpenTelemetry SDK's default sampler, starts its server span from that header. It calls the licence service over HTTP with the context injected, and adds a transcode job with the context stored in the job; the transcode worker extracts it before starting its span. The licence service and the worker also use the default sampler.
Which of R's spans reach an exporter?
Question 4 of 6
ExamHall's Next.js server, NestJS API and grading worker each run the OpenTelemetry SDK, and a submit trace starts at the Next.js server. The team must keep every trace in which any span records an error, plus 1 in 20 of the other traces. Grading errors are raised in the worker, the last service a submit trace reaches.
Where can that rule be applied?
Question 5 of 6
A ticket shop's gateway keeps 1 in 4 traces. Behind it, an old seat-hold service on the OpenTelemetry SDK's default sampler calls the seat service, written before the team used the propagator:
The seat service extracts the header and also uses the default sampler. Over 40 orders, the trace backend holds seat-service spans whose parent span never reaches it.
Which line causes them?
1 const span = tracer.startSpan('seat.hold', {}, incoming);2 const { traceId, spanId } = span.spanContext();3 const headers = { traceparent: `00-${traceId}-${spanId}-01` };4 seats.hold(orderId, headers);5 span.end();Question 6 of 6
A payment API adds a settle-payment job with its context stored in job.data.trace. The settle worker extracts job.data.trace and runs its handler inside a settle span, so settle sits in the checkout's trace. Inside that span the handler adds a send-receipt job, whose worker also extracts job.data.trace before its receipt span. Ops want receipt in the checkout's trace as a child of settle.
What should the settle handler store in send-receipt's job.data.trace?
Go deeper
Pro tips
- Test propagation the way you test logic: export spans to an in-memory exporter and assert that, at each hop, the receiving span's trace-id equals the sender's and its parent is the sender's span id.
When: after adding or changing a hop between processes (steps 4–5). Verify:
tip in-memory export · hops checked 1 · trace-id equal true · parent is the sender's span trueStatus: RAN.
Source
The OpenTelemetry docs can change: the facts below are as of 2026-10-01. Valid as of 2026-10-01.
- W3C, Trace Context, W3C Recommendation 23 November 2021: https://www.w3.org/TR/trace-context/ (
w3c-trace-context.txt). The curriculum row's first source. Quoted:trace-id = 32HEXDIGLC ; 16 bytes array identifier. All zeroes forbidden;parent-id = 16HEXDIGLC ; 8 bytes array identifier. All zeroes forbidden; "If the trace-id value is invalid (for example if it contains non-allowed characters or all zeros), vendors MUST ignore the traceparent."; "Update parent-id: The value of property parent-id MUST be set to a value representing the ID of the current operation."; "If a component needs to make a recording decision - it SHOULD respect the sampled flag value." - OpenTelemetry, Context propagation: https://opentelemetry.io/docs/concepts/context-propagation/ (
otel-context-propagation.txt). The row's second source. Quoted: "Service B uses these values to create a new span that belongs to the same trace, setting the span from Service A as its parent."; "On the side of the sender, the context is injected into the carrier, for example, into the headers of an HTTP request. In other cases, you need to find a place that can store metadata for your request."; "On the receiving side, the context is extracted from the carrier." Supports steps 2, 4, 5 and 7. - OpenTelemetry, Collector: https://opentelemetry.io/docs/collector/ (
otel-collector.txt) and Architecture: https://opentelemetry.io/docs/collector/architecture/ (otel-collector-arch.txt). The row names the Collector. Quoted: "The OpenTelemetry Collector offers a vendor-agnostic implementation of how to receive, process and export telemetry data."; "A processor might also drop the data if it's sampling or filtering." - OpenTelemetry, Sampling: https://opentelemetry.io/docs/concepts/sampling/ (
otel-sampling.txt), a page of the row's OpenTelemetry docs. Quoted: "Head sampling is a sampling technique used to make a sampling decision as early as possible."; "For example, you cannot ensure that all traces with an error within them are sampled with head sampling alone."; "The OpenTelemetry Collector includes the following sampling processors: Probabilistic Sampling Processor Tail Sampling Processor". - OpenTelemetry, JavaScript: Sampling: https://opentelemetry.io/docs/languages/js/sampling/ (
otel-js-sampling.txt). Quoted: "The JavaScript SDK offers several head samplers." and "By default, all spans are sampled, and thus, 100% of traces are sampled." The second sentence holds for a root span: the executed SDK 2.11.0 default isParentBased{root=AlwaysOnSampler, …, remoteParentNotSampled=AlwaysOffSampler, …}, so a child of an unsampled parent is not sampled. Steps 9–10 follow the run. - OpenTelemetry, What is OpenTelemetry?: https://opentelemetry.io/docs/what-is-opentelemetry/ (
otel-what-is.txt). Quoted: "OpenTelemetry is not an observability backend itself." and "The backend (storage) and the frontend (visualization) of telemetry data are intentionally left to other tools." - Background, nothing keyed on them alone: OpenTelemetry JavaScript: Propagation (https://opentelemetry.io/docs/languages/js/propagation/,
otel-js-propagation.txt) and Traces (https://opentelemetry.io/docs/concepts/signals/traces/,otel-traces.txt). Traces lists a span's fields, among them "Start and End Timestamps"; it supports step 14, which no item serves.
Related lessons
Checked against the tools and docs as of 2026-10-01.