Self-hosted service
snok/asgi-correlation-id avatar
snok/asgi-correlation-id

asgi-correlation-id: request ID propagation for ASGI applications

Request ID propagation for ASGI apps

642 stars38 forksPythonMIT

At a glance

What is it?
asgi-correlation-id is a small ASGI middleware that reads a correlation ID from an incoming request header or generates one, and a companion log filter that puts that ID into every log line the request produces. It is two steps of wiring rather than a framework, it validates incoming IDs before trusting them, and it says exactly what you have to change in your CORS setup for it to work across origins.
Who is it for?
asgi-correlation-id suits a FastAPI or Starlette service whose logs are hard to correlate by hand, especially one behind a proxy that already sets a request ID header, since the default validator accepts nginx request IDs and OpenTelemetry trace IDs as they are. It is not worth the wiring for a small service where a single logger and a context variable would do, and it does nothing for you in a synchronous worker or a non-ASGI framework.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 136 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is untangling log lines by hand

The middleware exists for one narrow purpose, and the README states it plainly. It reads or generates a correlation ID for each incoming request, and those correlation IDs can then be added to your logs, making it simple to retrieve all logs generated from a single HTTP request. The example makes the benefit obvious. Before, your log output is a stream of lines with no way to tell which request produced which warning, and a single user action that fans out into five internal calls produces five warnings from two different modules with nothing linking them. After, the same lines carry a bracketed identifier, and lines sharing an identifier belong to the same request. The topics attached to the repository, celery, sentry and tracing, describe where the pain is worst. This is a service-side observability tool for Python web developers, not a tracing system: it does not measure spans or ship telemetry anywhere, it labels lines so that a human or a log query can group them.

Read the header, or make one up

The behaviour is easy to state. When the middleware detects a correlation ID HTTP header in an incoming request, the ID is stored. If no header is found, a correlation ID is generated for the request instead. The header it looks at by default is X-Request-ID, and the package can be set to any key, with X-Correlation-ID named as the other common choice. That two-path behaviour is the interesting design decision, and it is what makes the middleware useful behind a proxy. If your ingress already stamps requests with a request ID, the middleware adopts that value, so the identifier in your application logs is the same string the edge log has, and a search across both layers works. If nothing is there, the middleware invents an identifier, so the logs are still groupable, just without a join key to the edge. Either way the application code does not change.

Adding the middleware takes one line

Installation is a single pip command:

bash
pip install asgi-correlation-id

And the middleware is added the way any ASGI framework allows. In FastAPI:

python
from fastapi import FastAPI

from asgi_correlation_id import CorrelationIdMiddleware

app = FastAPI()
app.add_middleware(CorrelationIdMiddleware)

or any other way your framework allows. The package depends only on starlette, at version 0.18 or newer, so it drops into a Starlette application unchanged, and the documentation says to substitute FastAPI with Starlette in all the examples for Starlette apps. The manifest declares Python 3.10 and newer, with classifiers for 3.10 through 3.14, and the package is marked as production stable. Note the dependency shape. This is not a FastAPI extension, it is a Starlette middleware, so it also works with anything else built on ASGI, which is why the ASGI framing in the project description matters more than it first appears.

The log filter is a separate step, and it is the one people miss

Adding the middleware does nothing on its own, because the identifier has to reach your log output. The package handles that with a logging filter, and the documentation is explicit that the setup has two parts. If your log config looks like a normal dict config with a formatter, a stream handler and your project logger, you add a filter to the config and reference it from the handler, and add the correlation_id placeholder to the format string. The filter takes two settings in the example, a uuid_length of 32 and a default_value of a dash, which is what appears in a line when no ID is present, for example a log emitted outside a request. If you use a JSON log formatter instead, the instruction is to add correlation-id: %(correlation_id)s to the list of properties. The default value of a dash is a small thing that saves confusion later, since a bare placeholder would otherwise print something like None and look like a bug in the filter.

The five knobs, and the validator that surprises people

The middleware has no required arguments, and the configuration block in the README shows the full set:

python
app.add_middleware(
    CorrelationIdMiddleware,
    header_name='X-Request-ID',
    update_request_header=True,
    generator=lambda: uuid4().hex,
    validator=is_valid_uuid4,
    transformer=lambda a: a,
)

The header_name decides which header value to read, defaulting to X-Request-ID. The generator produces new IDs when no header arrives, defaulting to a UUID4 hex string, and the documentation suggests nanoid or a custom function if you prefer something else. The transformer is the identity by default and exists for people who want to alter incoming or generated values, and the documentation says most users will not need it. The validator is the one with a real opinion. By default it accepts any 32-character hex string that can be parsed as a UUID, which includes standard UUIDv4s, nginx-generated request IDs and OpenTelemetry trace IDs, and it rejects non-hex or malformed values, generating a new ID instead. If you want to allow any header value you set the validator to None, and for stricter UUIDv4 checking you pass your own. The practical consequence is that a gateway sending an unexpected ID format is not an error, it is a silently replaced value.

The request header is rewritten on purpose

One default deserves explanation because it surprises people reading the request object downstream. The update_request_header option is True, and its description says it updates the incoming request's header value with the generated correlation ID, to support use cases where something relies on the presence of the request header, like various tracing middlewares. The reasoning is sound. A tracing middleware further down the stack expects to find a request ID on the request, and if the correlation middleware generated an ID because none arrived, writing it back means the tracing layer can pick it up instead of minting a second, different identifier. The trade-off is that the request your endpoint receives is not byte for byte the request that arrived, and if you log raw headers for debugging you will see the substituted value. If your application treats that header as untrusted input, set the option to False and pass the identifier through the logging filter only.

CORS needs two specific changes

The last part of the documentation is a short but genuinely useful warning about cross-origin resource sharing. If your frontend JavaScript is served from a different origin and calls your API, you have to ensure two things. You must permit the correlation ID header in the incoming requests' HTTP headers so the value can be reused by the middleware, and you must add the correlation ID header to the allowlist in the response headers so the browser can read it. The documentation says this can be best accomplished with a dedicated middleware, and the explanation for why is that the browser has to be allowed both to send the header and to see the response header. This is where a lot of people conclude the package is broken. The middleware is working, the browser is refusing to hand your JavaScript a value it was not allowed to read, and the fix belongs in the CORS configuration rather than in the correlation code.

Editorial conclusion

asgi-correlation-id suits a FastAPI or Starlette service whose logs are hard to correlate by hand, especially one behind a proxy that already sets a request ID header, since the default validator accepts nginx request IDs and OpenTelemetry trace IDs as they are. It is not worth the wiring for a small service where a single logger and a context variable would do, and it does nothing for you in a synchronous worker or a non-ASGI framework. Before adopting it, check that your gateway sends a header the default validator will accept, since an unexpected format is silently replaced by a fresh ID, and add the header to your CORS allowlist before you conclude the feature is broken in the browser.

Frequently asked questions

How do I install and add asgi-correlation-id to a FastAPI app?

Install it with pip install asgi-correlation-id, then call app.add_middleware(CorrelationIdMiddleware) after creating the app. The package depends only on starlette, so the same middleware works in a Starlette application, and the documentation says to substitute Starlette for FastAPI in the examples.

Which HTTP header does the middleware read the correlation ID from?

X-Request-ID by default, and the header_name argument can set it to any key, with X-Correlation-ID named as the other common choice. If the header is absent, the middleware generates an ID instead, so requests are always labelled.

What does the default validator accept and reject?

It accepts any 32-character hex string that can be parsed as a UUID, which includes standard UUIDv4s, nginx-generated request IDs and OpenTelemetry trace IDs, and rejects non-hex or malformed values, generating a new ID instead. Set the validator to None to accept any header value, or pass your own for stricter UUIDv4 checking.

What do I need to change for CORS to work with correlation IDs?

Permit the correlation ID header in incoming request headers so the middleware can reuse the value, and add the header to the allowlist in the response headers so the browser can read it. The documentation suggests using a dedicated middleware for this.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. snok/asgi-correlation-id on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/snok-asgi-correlation-id.svg)](https://hysenlabs.com/projects/snok-asgi-correlation-id)

Was this useful?