Correlating Across Services
Beyond a single request
A request to POST /v2/orders does more than return a response. It might:
- Create the order (database INSERT)
- Charge the payment (external API call to Stripe)
- Enqueue a confirmation email (background job)
- Notify inventory (HTTP call to another service)
Each of these operations produces logs in different systems. Without correlation, you have four separate log streams that cannot be connected.
The correlation ID
The request ID becomes a correlation ID when it is passed beyond the original request — to background jobs, external API calls, and downstream services. Same ID, same purpose: trace a chain of operations back to the original trigger.
Passing to background jobs
// In the route handler
const log = c.locals.log;
log.info("enqueueing confirmation email");
enqueueJob("send_order_confirmation", {
orderId: order.id,
correlationId: c.locals.requestId, // Pass the request ID to the job
}); The job handler uses the correlation ID in its own logger:
// In the job handler
export async function handleSendConfirmation(payload: any) {
const jobLog = logger.child({
job: "send_order_confirmation",
correlationId: payload.correlationId,
orderId: payload.orderId,
});
jobLog.info("processing job");
await sendEmail(payload.to, "Order Confirmed", body);
jobLog.info("job completed");
} Now the job’s logs include correlationId: "req_a1b2c3" — the same ID as the original HTTP request. Search for req_a1b2c3 and you see: the request, the order creation, the job enqueue, and the email send.
[!NOTE] The Background Jobs course stored job payloads as JSON. Adding
correlationIdto the payload is a simple extension that enables cross-system tracing.
Passing to external API calls
When calling external services, pass the correlation ID as a header:
async function callInventoryService(
orderId: string,
items: any[],
correlationId: string,
log: Logger,
) {
log.info("notifying inventory service", { orderId, itemCount: items.length });
const response = await fetch("https://inventory.internal/v1/reserve", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Request-Id": correlationId, // Pass correlation ID
},
body: JSON.stringify({ orderId, items }),
});
log.info("inventory service responded", { status: response.status });
return response;
} The inventory service receives X-Request-Id: req_a1b2c3, uses it in its own logs, and the entire chain is traceable.
Passing to downstream services
If your API calls another internal API, forward the request ID:
async function fetchUserProfile(userId: string, correlationId: string): Promise<UserProfile> {
const response = await fetch(`https://users.internal/v1/users/${userId}`, {
headers: { "X-Request-Id": correlationId },
});
return response.json();
} The user service logs the same correlation ID. A single search finds logs across all services involved in the original request.
The full trace
Search for req_a1b2c3:
{"service":"book-catalog","requestId":"req_a1b2c3","message":"request started","method":"POST","path":"/v2/orders"}
{"service":"book-catalog","requestId":"req_a1b2c3","message":"order created","orderId":"order-99"}
{"service":"book-catalog","requestId":"req_a1b2c3","message":"enqueueing confirmation email","orderId":"order-99"}
{"service":"book-catalog","requestId":"req_a1b2c3","message":"request completed","status":201,"duration":145}
{"service":"email-worker","correlationId":"req_a1b2c3","message":"processing job","job":"send_order_confirmation"}
{"service":"email-worker","correlationId":"req_a1b2c3","message":"email sent","to":"[email protected]"}
{"service":"inventory","requestId":"req_a1b2c3","message":"stock reserved","orderId":"order-99","items":3} Three services, one correlation ID, a complete picture of what happened.
Exercises
Exercise 1: Pass the request ID to a background job as correlationId. Log the job processing with the same ID. Search for the ID and find both the request and job logs.
Exercise 2: Pass X-Request-Id to an external service call. Verify the header is sent.
Exercise 3: Simulate a three-service trace. Log in each service with the same correlation ID. Search and verify the complete chain.
Why pass the request ID to background jobs as a correlation ID?