X request id header
Send one request with no header and one carrying your own value, then read the response headers with curl -s -D - -o /dev/null -H 'X-Request-Id: qa-smoke-0042' http://localhost:9417/orders. A service that handles request ids echoes the value you sent, generates one when you send none, and writes the same id into its log.
Why check this
Run this on staging sign-off and again after any change to the gateway, the load balancer or the logging layer. The failure it prevents is the one that costs a day: a customer reports that checkout returned an error at 14:03, the response carried no id, and the only way back to the log line is a full text search over every service for that minute.
The check answers two separate questions. Does the response carry an id at all, and is that id the one the client sent. A service can pass the first and fail the second, which looks correct in a browser and breaks every time a tester quotes an id from a failed request and the log has no such value. Both halves are read from the wire, not from the configuration.
Prerequisites
- Node 22. Confirm with
node --version. - curl 8, any build. See the curl manual for
-D -, which prints response headers. - The service below, saved as
reqid.mjsand started withnode reqid.mjs > api.log. Three routes stand for three real behaviours:/ordersaccepts an inbound id,/mintalways generates its own,/silentlogs the id and never returns it.
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
const log = (o) => console.log(JSON.stringify({ ts: new Date().toISOString(), svc: 'api', ...o }));
createServer((req, res) => {
const incoming = req.headers['x-request-id'];
const id = req.url === '/mint' ? randomUUID() : incoming ?? randomUUID();
if (req.url !== '/silent') res.setHeader('X-Request-Id', id);
log({ level: 'info', msg: 'request', path: req.url, requestId: id,
source: incoming === undefined ? 'generated' : 'client' });
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true }));
}).listen(9417, '127.0.0.1', () => log({ level: 'info', msg: 'listening', port: 9417 }));
Steps
- Step 1.
Send a request with no request id and print the response headers.
curl -s -D - -o /dev/null http://127.0.0.1:9417/ordersHTTP/1.1 200 OK X-Request-Id: 4e7cf2cd-1123-40f8-8906-8cce7a0cfc37 Content-Type: application/json Date: Fri, 11 Sep 2026 22:54:19 GMT Connection: keep-alive Keep-Alive: timeout=5 Content-Length: 11The id is a UUID the service invented, because nothing upstream supplied one.
- Step 2.
Send your own id and confirm the response carries it back unchanged.
curl -s -D - -o /dev/null -H 'X-Request-Id: qa-smoke-0042' http://127.0.0.1:9417/orders | grep -i 'request-id'X-Request-Id: qa-smoke-0042A value you chose is worth more than a UUID during a test run. You can grep for
qa-smoke-0042across every service without copying an id out of a response first. - Step 3.
Repeat against a route that generates its own id regardless of the inbound header.
curl -s -D - -o /dev/null -H 'X-Request-Id: qa-smoke-0043' http://127.0.0.1:9417/mint | grep -i 'request-id'X-Request-Id: ab5fea49-b000-49a6-9385-0ac66061ba80The header is present and the value is not yours. The trace now starts at the edge, so an id printed by a mobile client or a browser console cannot be found in any server log.
- Step 4.
Count the header on a route that logs the id but does not return it.
curl -s -D - -o /dev/null -H 'X-Request-Id: qa-smoke-0044' http://127.0.0.1:9417/silent | grep -ci 'request-id'0Zero is the defect this page exists to catch. The service knows the id and keeps it to itself, so support has nothing to quote from the response.
- Step 5.
Read the service log and match each request to its line.
cat api.log{"ts":"2026-09-11T22:54:00.587Z","svc":"api","level":"info","msg":"listening","port":9417} {"ts":"2026-09-11T22:54:19.232Z","svc":"api","level":"info","msg":"request","path":"/orders","requestId":"4e7cf2cd-1123-40f8-8906-8cce7a0cfc37","source":"generated"} {"ts":"2026-09-11T22:54:26.652Z","svc":"api","level":"info","msg":"request","path":"/orders","requestId":"qa-smoke-0042","source":"client"} {"ts":"2026-09-11T22:54:26.727Z","svc":"api","level":"info","msg":"request","path":"/mint","requestId":"ab5fea49-b000-49a6-9385-0ac66061ba80","source":"client"} {"ts":"2026-09-11T22:54:26.790Z","svc":"api","level":"info","msg":"request","path":"/silent","requestId":"qa-smoke-0044","source":"client"} {"ts":"2026-09-11T22:54:26.874Z","svc":"api","level":"info","msg":"request","path":"/orders","requestId":"","source":"client"}Line 2 holds the generated id from step 1 and matches the response. Line 3 holds
qa-smoke-0042, so step 2 is traceable end to end. Line 4 is the one to read twice: the path is/mint, the id is a UUID, andqa-smoke-0043appears nowhere in the file, whichgrep -c 'qa-smoke-0043' api.logconfirms with a0. Line 5 proves that the/silentid was known and withheld. Line 6 is described under Common mistakes. - Step 6.
Compare with a public API that runs this pattern in production.
curl -sI -H 'X-Request-Id: qa-smoke-0042' https://api.github.com/ | grep -i -E 'request-id|^HTTP'HTTP/2 200 access-control-expose-headers: ETag, Link, Location, Retry-After, …, X-GitHub-Request-Id, Deprecation, Sunset, Warning x-github-request-id: D8A8:66109:3073A57:2EEEC01:6AA486B6GitHub discards the id you sent, returns its own under a vendor name, and lists that name in
access-control-expose-headers. Read on 2026-09-12; the id changes on every request.
How to read the result
| What you see | What it means | What to do | | --- | --- | --- | | The header comes back holding the value you sent | Client ids survive the edge | Nothing. Quote this id in every bug report. | | The header comes back holding a different value | The edge mints its own id | Fine by design. Say so in the API docs, since a client id is then a local label only. | | No such header on any route | The response is untraceable | Add it at the outermost layer, so every route inherits it. | | The header is present and the log has no matching line | Two different ids in one request path | Find the layer that regenerates. A proxy or a retry wrapper is the usual one. | | The header is present and empty | An inbound empty value was accepted | Validate before use, as in the first mistake below. |
Common mistakes
What to check next
- How to check correlation id in logs: the same id has to reach the services behind the edge, which is a separate check.
- Trace id in logs: where the id comes from when a tracing library, not the application, owns it.
- Json logging format: grepping for an id is only reliable when the id is a field, not a fragment of a message.
- Access log format: the proxy access log needs the same id, or the hop before the application stays dark.
FAQ
What is the difference between X-Request-Id and X-Correlation-Id?
By convention a request id names one HTTP request, and a correlation id names the whole business operation that may span several requests and services. Neither name is standardised, so read the service code or the gateway configuration rather than assuming from the header name.
Is it X-Request-Id or Request-Id?
Both are in production use, and RFC 6648 has discouraged the X- prefix since 2012. Header names are case insensitive, so grep -i is the safe way to look. Pick one name per system and reject the other at the edge.
What does a request id header look like?
Most often a UUID, as in step 1. Any short opaque string works. GitHub uses a colon separated hex form, shown in step 6. Constrain the accepted length and character set, since the value ends up in log files and dashboards.
Which component should generate the id?
The outermost one that sees every request, usually the reverse proxy or the API gateway. It accepts a client id when the client is trusted and generates one when it is not. Services behind it never invent a second id.
Verified
Verified by Maks Vernynode 22.23.2curl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic6 minpublished updated Maks Verny