Propagate Request IDs Through Logs
Generate or accept a traceable request ID, attach it to every log line and response, and propagate it to downstream calls so one user action can be followed end to end.
Task
Add request correlation IDs to an HTTP service without trusting arbitrary client input.
When to use this
Debugging a failed request requires searching logs across services, but entries cannot be correlated because each log line lacks a shared identifier.
Prerequisites
- A structured logging library or JSON log output
- Middleware support in the HTTP framework
- A convention for inbound trace headers
Steps
1. Accept a W3C traceparent or a validated x-request-id from the caller; otherwise generate a new UUID. Reject values containing control characters or excessive length.
import { randomUUID } from 'node:crypto';
const incoming = req.get('x-request-id');
const requestId = incoming && /^[A-Za-z0-9._-]{8,128}$/.test(incoming)
? incoming
: randomUUID();
Expected output: Every request has a safe, bounded correlation ID.
2. Store the ID in AsyncLocalStorage (or framework request context) and return it in the response header.
const storage = new AsyncLocalStorage();
app.use((req, res, next) => {
storage.run({ requestId }, () => {
res.set('x-request-id', requestId);
next();
});
});
Expected output: Handlers and loggers can read the same ID without passing it through every function.
3. Emit structured logs with the request ID, method, path, status, and duration. Never log authorization headers or request bodies containing secrets.
logger.info({ requestId, method: req.method, path: req.path, status: res.statusCode, durationMs });
Expected output: Log entries are JSON objects correlated by requestId.
4. Forward the ID to outbound HTTP calls and include it in error responses. Verify by sending a request and searching logs for the returned ID.
curl -i http://localhost:3000/orders && grep 'requestId' logs/app.log | tail -5Expected output: The response header and every related log line share the same ID.
Troubleshooting
Problem
Logs from async callbacks lose the ID
Solution
Use AsyncLocalStorage or pass an explicit context; do not rely on a module-level variable.
Problem
Clients send conflicting IDs
Solution
Validate the format and decide whether to trust it; for internal services, generate a new span while preserving the trace context.
Limitations
- A request ID is not a security boundary; never use it for authorization.
- High-cardinality IDs are fine in logs but should not become metric labels.
One ID, every layer
The goal is simple: a user reports an error, and one search finds the request, its downstream calls, and the related log lines. That requires generating or accepting an ID at the edge, storing it in request context, returning it, and forwarding it.
app.use((req, res, next) => {
const id = safeRequestId(req.get('x-request-id')) ?? randomUUID();
res.set('x-request-id', id);
storage.run({ requestId: id }, next);
});
app.get('/orders', async (req, res) => {
const { requestId } = storage.getStore();
logger.info({ requestId, path: req.path }, 'handling request');
res.json({ ok: true });
});
Verify
- The response includes
x-request-id. - Every log line for the request contains the same ID.
- An outbound call receives the ID in its headers.
- Invalid or oversized inbound IDs are replaced, not trusted.
- Error responses include the ID so users can report it.
Pitfalls
Module-level variables leak IDs across concurrent requests. Logging entire headers or bodies can expose credentials. Using request IDs as metric labels creates unbounded cardinality. Keep the ID opaque, bounded, and separate from authentication.