How to Identify Individual API Requests
Brew #4·October 15, 2026·8 minutes read
A request ID is an opaque identifier assigned to an HTTP request that allows the request to be uniquely identified within the application, in its logs, and in the HTTP response returned to the client.
Incorrect request ID handling can cause:
- Logs from different requests to be grouped under the same identifier.
- Clients to inject arbitrary identifiers into application logs.
- Request ordering or application data to be exposed through predictable or data-derived IDs.
The short answer
The most common way to identify individual API requests is to:
- Generate a new UUIDv4 for every incoming request or use an infrastructure-generated ID.
- Store the request ID in the request object.
- Return the request ID in the
X-Request-IDresponse header. - Expose the
X-Request-IDheader through CORS if a browser client needs to read it from a cross-origin response. - Configure a request-scoped logger to use the request ID so every request-specific log contains the same identifier.
Creating a request ID handling middleware
The easiest way to assign a request ID to every incoming request is to use a middleware function that runs before the rest of the application, assigns the request ID, and then forwards the request so the same value is available to all subsequent components.
In this Brew, we'll use the following Express.js middleware as a base and progressively add each part of the implementation:
import type {
Request,
Response,
NextFunction
} from 'express';
function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
next();
}
export default requestIdMiddleware;
Mounting the request ID handling middleware
To make the request ID available to every subsequent component that handles the request, the request ID handling middleware should be mounted as early as possible in the request-processing pipeline.
import express from 'express';
import requestIdMiddleware from '../middlewares/request-id.js';
const app = express();
app.use(requestIdMiddleware);
// Request logging
// Authentication
// Validation
// ...
// Routes
Generating a unique request ID
Every incoming HTTP request must be assigned a unique request ID that is generated independently of other requests.
This means that a request ID should not be:
- Sequential or derived from application data to avoid exposing request ordering, traffic patterns, or information about the request itself.
- Reused as an idempotency key or an identifier of the underlying business operation.
For example, if a client retries the same operation, the original request and the retry must have different IDs:
POST /subscriptions d293b76a-6e34-4e10-abb2-7fa958166a28
POST /subscriptions 83650d24-eacc-4a70-b75e-e5ba2fc9dedb
For example, in Node.js, you can generate a UUIDv4 for each incoming request using crypto.randomUUID(), as UUIDv4 collisions are extremely unlikely.
import { randomUUID } from 'node:crypto';
function generateRequestId(): string {
return randomUUID();
}
Storing the request ID in the request object
Once generated, the request ID should be stored in the request object under an explicit property, such as requestId.
For example, in Express.js, you can first extend the built-in Request type with a requestId property:
export {};
declare global {
namespace Express {
interface Request {
requestId: string;
}
}
}
Then assign the generated request ID to the requestId property of the request before forwarding it to the next component:
import { randomUUID } from 'node:crypto';
import type {
Request,
Response,
NextFunction
} from 'express';
function generateRequestId(): string {
return randomUUID();
}
function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const requestId = generateRequestId();
req.requestId = requestId;
next();
}
export default requestIdMiddleware;
Returning the request ID in the response
A common convention is to return the request ID in the non-standard X-Request-ID HTTP response header. This lets the client log the value, display it with an error, or provide it in a support report so the corresponding request can be found in the API logs.
For example, in Express.js, you can use the set() method of the response object:
// ...
function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const requestId = generateRequestId();
req.requestId = requestId;
res.set('X-Request-ID', requestId);
next();
}
export default requestIdMiddleware;
Exposing the request ID to browser clients
For cross-origin requests, the X-Request-ID response header must be exposed through CORS before browser JavaScript can access it.
For example, in Express.js, you can add X-Request-ID to the exposed response headers of the cors middleware:
app.use(
cors({
exposedHeaders: ['X-Request-ID']
})
);
The client can then retrieve the request ID from the response:
response.headers.get('X-Request-ID');
Adding the request ID to application logs
To associate application logs with the request that produced them, you must configure your HTTP logger to use the request ID present in the request object.
For example, in pino-http, the genReqId option is used to define the identifier used for the request and the request-scoped logger created by the middleware:
import pinoHttp from 'pino-http';
import type {
Request,
Response
} from 'express';
const httpLogger = pinoHttp<Request, Response>({
genReqId(req) {
return req.requestId;
}
});
export default httpLogger;
Mount the HTTP logger after the request ID handling middleware so req.requestId is already available:
app.use(requestIdMiddleware);
app.use(httpLogger);
Logs written through the request-scoped logger available on req.log will automatically include the request ID.
req.log.info(
{ subscriberId },
'Subscriber created.'
);
Using infrastructure-generated request IDs
In some architectures, request ID generation can be delegated to the infrastructure. In this case, a reverse proxy or an API gateway assigns the request ID before forwarding the request to the application, allowing the same identifier to be included in both infrastructure and application logs.
For example, NGINX exposes a $request_id variable containing 16 random bytes represented in hexadecimal, whose value can be forwarded to the application in the X-Request-ID request header.
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://api;
}
This value can then be used by the request ID middleware instead of generating a new one:
// ...
function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const requestId = req.get('x-request-id') ?? generateRequestId();
req.requestId = requestId;
res.set('X-Request-ID', requestId);
next();
}
This approach, however, assumes that requests carrying a trusted X-Request-ID cannot bypass the infrastructure and reach the application directly. Otherwise, a public client could supply the value that the application treats as trusted.
In this configuration,
proxy_set_header X-Request-ID $request_idreplaces any client-suppliedX-Request-IDvalue before the request is forwarded to the application.
The same request ID will then appear in both the NGINX access logs and the application logs:
log_format api '$remote_addr "$request" $status request_id=$request_id';
access_log /var/log/nginx/api_access.log api;
Production considerations
Validating infrastructure-generated request IDs
To prevent control characters or unexpected input from entering application logs, infrastructure-provided request IDs should be validated against the exact format generated by the trusted infrastructure. Invalid values should then either be rejected or replaced with a newly generated request ID.
Keeping request IDs out of metrics
Request IDs should be stored in logs rather than used as metric labels or tags. Because each request normally has a different ID, using request IDs in metrics can create a separate metric series for every request, which can quickly increase storage and query costs.
Distinguishing request IDs from trace IDs
A request ID identifies a single HTTP request handled by one API or service. A trace ID identifies an entire distributed operation, which can include multiple HTTP requests and other operations across several services. In distributed tracing, the same trace ID is shared across the operations that belong to the same trace, while each individual operation is represented by its own span.