python 97 lines · 4 tabs

Propagating a Correlation ID Through contextvars in FastAPI

Shared by codesnips Aug 2026
4 tabs
import contextvars
import uuid

_correlation_id: contextvars.ContextVar[str] = contextvars.ContextVar(
    "correlation_id", default="-"
)


def set_correlation_id(value):
    return _correlation_id.set(value)


def reset_correlation_id(token):
    _correlation_id.reset(token)


def get_correlation_id():
    return _correlation_id.get()


def new_correlation_id():
    return uuid.uuid4().hex
4 files · python Explain with highlit

This snippet shows how to trace a single request across every layer of an async FastAPI service by threading a correlation ID through a contextvars.ContextVar instead of passing it down as a function argument. The core problem is that in an async application, many coroutines run interleaved on the same event loop, so a plain module-level global would be shared and clobbered between concurrent requests. contextvars solves this by giving each logical execution context — including tasks spawned per request — its own isolated copy of the variable, which makes it the correct primitive for request-scoped state under asyncio.

In correlation.py, the ContextVar named _correlation_id holds the current request's ID with a safe default. set_correlation_id returns a Token so the value can be restored later, and get_correlation_id is the read side used everywhere else. The lightweight helpers keep the rest of the codebase free of any knowledge of how the ID is stored, so the storage mechanism could change without touching call sites.

middleware.py is where the ID enters the system. CorrelationIdMiddleware reads an incoming X-Request-ID header for propagation across services, falling back to a fresh uuid4 when a client did not supply one. It sets the value with set_correlation_id, and crucially resets it in a finally block using the returned token so the context does not leak into whatever runs next on that worker. The same ID is echoed back on the response header so callers and load balancers can correlate too.

logging_config.py wires the ID into every log line. The CorrelationIdFilter pulls the current value via get_correlation_id and attaches it as a record attribute, letting the Formatter include %(correlation_id)s without any per-call plumbing. Because the filter reads the ContextVar at emit time, deeply nested service and repository code logs the right ID automatically.

app.py ties it together: the middleware is registered, logging is configured at startup, and the handler and a nested service call both log without ever mentioning the correlation ID. The trade-off is that contextvars state is implicit, so care must be taken with manually created tasks, which do copy the context at creation time, and with thread pools, which do not — a common pitfall when offloading blocking work.


Related snips

Share this code

Here's the card — post it anywhere.

Propagating a Correlation ID Through contextvars in FastAPI — share card
Link copied