# 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.

Source: https://howhttpworks.com/headers/x-request-id
Last reviewed: 2026-10-04

> **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

```http
GET /orders/42 HTTP/1.1
Host: api.example.com
X-Request-ID: 7f3c1d0a9b2e4f5c8d6e1a2b3c4d5e6f
```

```http
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:

```nginx
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:

```javascript
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](https://howhttpworks.com/headers/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

```bash
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?
```

## Related

- [Via](https://howhttpworks.com/headers/via), [X-Forwarded-For](https://howhttpworks.com/headers/x-forwarded-for), [Server-Timing](https://howhttpworks.com/headers/server-timing), [X-Response-Time](https://howhttpworks.com/headers/x-response-time)
