When a customer reports a failed request, “check the logs” is not enough if the logs cannot be connected to that request. A correlation ID gives one operation a stable handle across the API, background work, and downstream calls. This tutorial uses Node.js AsyncLocalStorage for application logs, explains how that differs from distributed trace context, and lays out the privacy and failure controls that make the pattern safe in production.
Prerequisites: define what the ID is for
You need a Node.js HTTP API, structured application logs, a request test client, and a way to inspect outbound calls. Decide first whether you need a log-correlation ID, a distributed trace ID, or both. A correlation ID is an application support handle; a trace ID belongs to a tracing system and follows the trace-context protocol. They can be related, but they should not be treated as interchangeable by accident.
Use an identifier that is opaque, bounded in length, and safe to put in logs and a response header. Generate one at the edge when the caller does not provide one. If you accept an incoming value, validate its characters and length, and decide whether your trust boundary permits it to be echoed. Never use an email address, access token, database key, or other sensitive value as the correlation ID.
- Choose a response header such as X-Request-ID and document it
- Generate a random ID with a collision-resistant library
- Limit accepted input to a safe character set and maximum length
- Keep the ID out of authorization and business decisions
- Define retention and access rules for logs containing the ID
Create the context at the HTTP boundary
Wrap each request in an AsyncLocalStorage context before calling application code. Node documents this API as a way to associate state with an asynchronous duration such as a web request, and its run() method makes the store available to asynchronous operations created inside the callback. That is the useful property: a logger deep in a service does not need every function signature to grow a requestId parameter.
Keep the store small and immutable by convention. A practical shape is { requestId, traceId } where traceId is present only when a trusted tracing library has established one. Set the response header before sending the response, and register the request context before parsing or invoking handlers so errors from the whole path can be correlated.
- Call storage.run(store, handler) once per incoming request
- Read the current store with getStore() inside log and client helpers
- Use a fallback such as “-” when code runs outside a request
- Do not mutate one shared store object between concurrent requests
- Test both synchronous logs and logs after await, timers, and promises
Make every log and response useful
Create one logger wrapper that adds requestId, route, service, level, and timestamp to structured records. Log the ID at request start and completion, on handled errors, and around important dependency calls. Keep messages human-readable, but let machines filter by fields rather than searching a sentence. Include a duration and status at the edge so support can find the complete request quickly.
Return the same ID in a response header, including for errors generated by the application boundary. The client can then give support a short value without exposing the full request or a trace payload. Do not promise that the header proves a request succeeded or that it is globally unique forever; it is a lookup handle with a defined retention window.
- Use structured fields rather than interpolating IDs into unparseable text
- Log safe dependency name, operation, duration, and outcome
- Preserve the ID when mapping exceptions to HTTP errors
- Return the ID in success and error responses
- Never log request bodies, tokens, cookies, or full provider payloads by default
Propagate context to downstream systems deliberately
For an internal HTTP call, send the correlation ID in the header your services agree on. For distributed tracing, use an OpenTelemetry propagator to inject the W3C traceparent and tracestate headers rather than hand-building them. The W3C specification defines those headers for propagating context between services; OpenTelemetry uses spans to describe the path and parent-child relationships of a request.
Queues need an explicit message envelope because AsyncLocalStorage does not cross a process boundary. Put a safe correlation ID and, where applicable, trace context in message metadata. The consumer should start a new local context for the job, preserve the originating ID for lookup, and also generate a job or attempt ID so one request that creates many jobs does not make their logs indistinguishable.
- Inject context at one outbound HTTP or messaging boundary
- Use standard trace propagation for traces; do not invent a trace format
- Treat incoming downstream headers as untrusted input
- Create a new context when a worker begins processing a message
- Keep request, job, and attempt IDs separate but linkable
Verify concurrency, failure, and trust boundaries
Write an integration test that sends two requests concurrently and records logs after an await, a timer, and an outbound call. Assert that each record has the correct ID and that no value leaks between requests. Send an invalid or oversized incoming ID and confirm the server replaces or rejects it according to the documented policy. Force a 4xx, 5xx, timeout, and uncaught error, then verify that every response still exposes a safe lookup ID.
Test process boundaries separately. Enqueue a job, stop the API, process it in a worker, and confirm that the worker logs link back to the originating request while using its own job context. If you export traces, compare the trace view with logs and make sure sampling or a missing parent does not make the application correlation ID disappear.
- Concurrent requests keep distinct IDs through asynchronous work
- Malformed, long, or attacker-chosen IDs cannot poison logs or headers
- Error handlers preserve the ID without returning stack traces
- Downstream calls and queue messages carry only approved context
- Worker retries retain the logical job link and add an attempt identifier
- Logs can be searched by ID without granting access to sensitive payloads
Production checklist and limitations
Correlation IDs improve the path from a customer report to an operator’s evidence; they do not replace metrics, traces, structured errors, or durable job state. AsyncLocalStorage can also lose context around unusual callback integrations or incorrectly managed event emitters, so verify the libraries and add an explicit context bridge when needed. A missing ID should be visible as a telemetry defect, not silently replaced everywhere without investigation.
Keep the implementation at the platform boundary and document it for every service. Decide how proxies handle the response header, whether external callers may supply an ID, how IDs are redacted from analytics, and how long logs remain searchable. The useful outcome is not a fancy identifier: it is a repeatable support workflow that connects request, dependency, worker, and failure evidence without weakening privacy.
- One owner maintains the ID format and propagation policy
- Request and job contexts are initialized before application work
- Logs, error responses, HTTP clients, and workers use the same documented fields
- Trace context is delegated to a standards-compliant tracing library
- Concurrency and process-boundary tests run in CI
- Dashboards expose requests with missing or malformed correlation data
- Retention and access controls cover all correlated logs and traces
- A runbook shows support how to search an ID and escalate the incident