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

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

  1. 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/orders
    
    HTTP/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: 11

    The id is a UUID the service invented, because nothing upstream supplied one.

  2. 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-0042

    A value you chose is worth more than a UUID during a test run. You can grep for qa-smoke-0042 across every service without copying an id out of a response first.

  3. 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-0ac66061ba80

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

  4. 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'
    
    0

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

  5. 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, and qa-smoke-0043 appears nowhere in the file, which grep -c 'qa-smoke-0043' api.log confirms with a 0. Line 5 proves that the /silent id was known and withheld. Line 6 is described under Common mistakes.

  6. 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:6AA486B6

    GitHub 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

Sign: The response carries X-Request-Id with an empty value.Cause: The service reads the inbound header with a nullish check, and an empty string is not null. curl -H 'X-Request-Id;' sends the header with no value, the check passes it through, and line 6 of the log shows requestId set to an empty string with source client. Validate the inbound id for shape and length before adopting it.
Sign: Browser JavaScript reads null for the header although curl shows it.Cause: A cross-origin response only exposes a short default set of headers to script. Any request id header has to be named in access-control-expose-headers, which is what GitHub does in step 6. Testing the header with curl alone will not reveal this.
Sign: Grepping the logs for an id from a response finds nothing.Cause: The service generated a fresh id for the response after logging, or a retry layer generated a second id for the second attempt. Step 3 shows the shape of this: the header is present, the value is real, and the id the client held never entered the log.

What to check next

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.

basic6 minpublished updated Maks Verny