How HTTP Works

Request header · non-standard

> X-Request-ID: 7f3c1d0a9b2e4f5c8d6e1a2b3c4d5e6f

X-Request-ID Header: Correlation IDs Across Services

X-Request-ID tags one request so you can find it in every log. nginx $request_id, Heroku behavior, propagation between services, and traceparent.

Reviewed 3 min readintermediate4 sourcesTry itMarkdown
Direction
Request
Category
Diagnostics
Status
Non-standard: De-facto correlation ID; the standard form is traceparent
JS can set it
Yes
CORS-safelisted
No: may trigger a preflight
On this page

TL;DR: X-Request-ID is an opaque per-request identifier, set at the edge (or by the client) and then copied into every downstream call and log line so one request can be followed across services. It is a convention, not a standard; the standard equivalent for tracing is traceparent.

How it flows

GET /orders/42 HTTP/1.1
Host: api.example.com
X-Request-ID: 7f3c1d0a9b2e4f5c8d6e1a2b3c4d5e6f
HTTP/1.1 200 OK
X-Request-ID: 7f3c1d0a9b2e4f5c8d6e1a2b3c4d5e6f
Content-Type: application/json

The rule is simple: use the ID if one arrived and passes validation, otherwise mint one; attach it to every log line; forward it on every outbound call; return it in the response.

Where it comes from

  • nginx can mint one. $request_id is a unique identifier generated from 16 random bytes, in hexadecimal (since 1.11.0). nginx does not automatically add it to headers; you do that:
log_format main '$remote_addr "$request" $status req_id=$request_id';
access_log /var/log/nginx/access.log main;

location / {
    proxy_set_header X-Request-ID $request_id;
    add_header X-Request-ID $request_id always;
    proxy_pass http://app;
}
  • Heroku router generates a request ID for each request and passes it to the app as X-Request-ID. A client can supply its own, 20 to 200 characters from ASCII letters, digits and + / = -; invalid values are ignored and replaced. The ID appears as request_id= in router logs, which is how you join router lines (including H13 errors) to application logs.
  • Frameworks: Rails exposes it via config.log_tags = [:request_id], and Django needs middleware such as django-log-request-id. Other platforms and load balancers vary in whether they set the header at all, so check a real response before relying on it.

Propagation in application code

Node with an AsyncLocalStorage so the ID follows async calls without being passed around:

import { AsyncLocalStorage } from 'node:async_hooks'
import { randomUUID } from 'node:crypto'

const als = new AsyncLocalStorage()
const VALID = /^[A-Za-z0-9+/=_-]{16,200}$/

export function requestId(req, res, next) {
  const incoming = req.get('x-request-id')
  const id = incoming && VALID.test(incoming) ? incoming : randomUUID()
  res.set('X-Request-ID', id)
  als.run({ id }, next)
}

export const currentId = () => als.getStore()?.id

// outbound call
await fetch('http://billing.internal/charge', {
  headers: { 'X-Request-ID': currentId() }
})

Log it as a structured field ("request_id": "..."), not inside the message text, so it is searchable.

Gotchas

  • Validate client values. An unbounded client string goes into your logs verbatim. Restrict length and charset, or discard it and keep it in a separate client_request_id field.
  • Retries share an ID, new requests do not. A retry by the client is the same logical request, so reusing the ID is fine; a fan-out call to five services all carry the parent’s ID.
  • CDNs and gateways may overwrite it. If the ID you see in app logs never matches what the client sent, a proxy minted a new one. Compare with a direct curl to the origin.
  • Not for secrets or users. Do not derive it from a user id or session, and do not treat it as unguessable authentication.
  • Case. HTTP header names are case-insensitive. Node lowercases them (req.headers['x-request-id']).

Alternative: W3C traceparent

When you already run OpenTelemetry or an APM, prefer traceparent: the trace id is parsed by tools, spans link into a tree, and vendors interoperate. Many teams keep both, putting the trace id in logs next to the request ID so non-engineers have a short value to quote.

Verify

curl -si https://api.example.com/health -H 'X-Request-ID: 7f3c1d0a9b2e4f5c8d6e1a2b3c4d5e6f' | grep -i x-request-id
curl -si https://api.example.com/health | grep -i x-request-id   # server-generated?

Frequently asked questions

Is X-Request-ID a standard header?

No. It is a de-facto convention with no RFC and no IANA registration for that exact name, which is why proxies, frameworks and cloud platforms each choose their own rules for it. For standardized cross-service tracing use the W3C traceparent header.

What does the nginx $request_id variable contain?

A unique identifier generated from 16 random bytes in hexadecimal, so 32 hex characters. It exists since nginx 1.11.0 and is generated per request, independent of any incoming header.

Should I trust an X-Request-ID sent by the client?

Accept it only after validating length and character set, or generate your own and log the client value separately. A client-supplied ID can be used for log injection or to collide with another request. Heroku, for example, only honors client IDs of 20 to 200 characters from a limited ASCII set and replaces anything else.

What is the difference between X-Request-ID and traceparent?

X-Request-ID is a free-form opaque string whose meaning is local to whoever set it. traceparent has a fixed format with a 16-byte trace id and 8-byte parent span id, is understood by OpenTelemetry and most APM vendors, and carries a sampling flag. Use traceparent for distributed tracing and X-Request-ID as the human-friendly handle in logs and error pages.

Should I return X-Request-ID in the response?

Yes. Echoing it in the response and in error bodies lets a user or support engineer quote one ID that finds the whole request in your logs.

Sources

  1. MDN Web Docs: HTTP headersdeveloper.mozilla.org
  2. nginx: $request_idnginx.org
  3. Heroku Dev Center: HTTP Request IDsdevcenter.heroku.com
  4. W3C Trace Contextw3.org

Keep going

Browse /search