skip to content

Describe how a correlation or request ID header (for example X-Request-Id or the W3C traceparent header) flows through a system of services, and what each service is responsible for doing with it.

level: juniorimportance: should knowfreq 50%

answer

  1. generate at edge if absent, else reuse
  2. bind to MDC / async context, clear after
  3. propagate outbound incl. queues and retries
  4. echo on response for support tickets
  5. traceparent = W3C trace id + span id + flags

basics

~20 s

The edge generates an ID if the incoming request has none, puts it in the logging context, echoes it on the response, and forwards it on every downstream call. Each service reuses the received value rather than minting a new one, so one ID ties all logs for a request together.

solid answer

~50 s

A correlation ID is a per-request identifier carried in an HTTP header so that logs from every service handling that request can be joined. The contract each service follows is small and must be uniform: 1. **Read** the header on inbound requests. **Generate** one (UUID or 128-bit random hex) only if absent — usually only the edge does this. 2. **Bind** it to the logging context (MDC, async-local storage) so every log line carries it automatically. 3. **Propagate** it verbatim on every outbound call, including async work: queue messages, jobs, retries. 4. **Echo** it on the response so clients and support can quote it. Use **`traceparent`** (W3C Trace Context) if you want interoperable distributed tracing — it encodes trace-id, span-id and sampling flags, and every tracing SDK understands it; a plain `X-Request-Id` remains useful as a human-quotable ticket ID. Two cautions: **never trust** a client-supplied ID for anything but logging (validate the format, cap the length), and remember unknown headers can be stripped by proxies unless allowlisted.

code

http · 8 lines
http
POST /orders HTTP/1.1
Host: api.example.com
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
x-request-id: 8f14e45f-ea3b-4d1f-9c9a-2b0d6f0a11c2

HTTP/1.1 201 Created
x-request-id: 8f14e45f-ea3b-4d1f-9c9a-2b0d6f0a11c2
Location: /orders/9931

go deeper

for a junior

Recall the four duties: read or generate, log, forward, echo — and that all services must reuse the same value.

for a middle

Add mechanics: MDC or async context binding and cleanup, propagation through queues and async work, and traceparent versus a plain request id header.

for a senior

Cover trust at the edge, validation and log-injection risk, propagation gaps in retries and workers, and headers being dropped by proxies that only forward an allowlist.

for a principal

Set the platform standard: which header names are canonical, adopting W3C Trace Context for interoperability, sampling strategy, and what identifiers may never be carried in transit metadata.

## The problem it solves One user action fans out across a gateway, three services and a worker. Without a shared key, you have five log streams and no way to line them up. A **correlation ID** (also request ID, trace ID) is a value minted once per request and carried in an HTTP header through every hop, so a single search returns the whole story. ## The lifecycle **Generate at the edge.** The first component that owns the request — CDN, ingress, API gateway or the outermost service — checks for the header. If absent, it creates one: a UUIDv4 or 16 random bytes hex-encoded. If present *and* the caller is trusted, it reuses it, so a client-side ID or a mobile app's ID ties through. **Bind to context.** The value goes into the logging context (SLF4J MDC, OpenTelemetry context, Node async-local storage) at the inbound filter, so developers do not have to thread it through function signatures and no log line silently loses it. Clear it when the request ends — on a pooled thread a leftover ID mislabels the *next* request, which is one of the classic bugs here. **Propagate outbound.** An interceptor on the HTTP client copies it onto every downstream request. The forgotten paths are the interesting ones: message-queue publishes (put it in message headers), scheduled jobs kicked off by a request, retries, and fire-and-forget async tasks. A gap in propagation shows up as a trace that stops halfway. **Echo on the response.** Returning the header lets a browser, a support ticket or an error page quote it. Many teams also embed it in error payloads so a user screenshot is enough to find the failure. ## X-Request-Id versus traceparent These are complementary, not rivals. - `X-Request-Id` (or a vendor-scoped equivalent) is a single opaque string. Cheap, human-quotable, no ecosystem needed. It has no standard — hence the zoo of `X-Request-ID`, `X-Correlation-ID`, `Request-Id`, `X-Amzn-Trace-Id`. - **`traceparent`** from W3C Trace Context is standardised: `version-traceid-spanid-flags`, e.g. `00-4bf92f...-00f067aa0ba902b7-01`. It carries a 32-hex-digit trace id, the caller's 16-hex-digit span id, and a sampled flag. Its companion **`tracestate`** carries vendor key-value data. Every modern tracing SDK emits and consumes these, so adopting them buys you interoperability with any backend — Jaeger, Tempo, a commercial APM — with no bespoke code. A common, sane setup: propagate `traceparent` for tracing and also surface the trace id as `X-Request-Id` for humans. ## Header naming and hygiene New headers should not use the `X-` prefix (RFC 6648 deprecated it), so a greenfield field is better named `Request-Id` or vendor-scoped like `Acme-Request-Id`. In reality `X-Request-Id` is entrenched and interoperable with existing proxies, which is a legitimate reason to keep it. Field names are case-insensitive; HTTP/2 and HTTP/3 send them lowercase on the wire, so never compare names case-sensitively. ## Trust and safety The header is attacker-controlled on the public edge. Consequences: - **Validate**: enforce a length cap and a character allowlist (hex or UUID shape). An unbounded value goes straight into your logs and can be used for log injection with newlines, or to blow up log volume. - **Never authorise on it.** It identifies a request, not a principal. - **Do not put user data in it.** Emails or account numbers in an ID header end up in every intermediary's access logs, including third parties. - Cardinality: it is fine as a log field, terrible as a metrics label — one time series per request will destroy a metrics backend. ## Why it sometimes vanishes Unknown headers are not guaranteed to survive. A CDN or load balancer may forward only an allowlist, some proxies drop non-standard fields, and header size limits can truncate. If IDs stop appearing at one hop, check that hop's forwarding configuration before suspecting the application.

  • Should a service generate a new ID if the inbound request already carries one?
    No, it should reuse the inbound value, otherwise the chain breaks at that hop and you get two disconnected halves of one request. Only the trust boundary decides whether to accept a caller-supplied value at all: at a public edge you may validate it or replace it, but internally you always propagate what you received.
  • What goes wrong if the ID is stored in a thread-local and not cleared?
    Thread pools reuse threads, so a stale value from a finished request labels the next unrelated request, producing logs that appear to belong to the wrong user or flow. Clear the context in a finally block or use a framework filter that scopes it to the request. The same hazard exists with async handoffs, where the context must be explicitly copied to the executing thread.

Like a shipping tracking number: assigned once at drop-off, printed on every handover form along the route, and quoted back to you when you ask what happened.

saying these in an interview costs you the question

  • Minting a fresh ID in every service instead of reusing the inbound one
  • Logging the ID but never forwarding it downstream, so the trace stops at the first hop
  • Using the header for authorisation or trusting it as an identity
  • Putting the ID into a metrics label, creating unbounded cardinality
  • Accepting an arbitrary-length client value straight into log lines without validation

context