# starlette-context: per-request context data for Starlette

> starlette-context is a Starlette middleware that creates a per-request context, lets handlers read and write into it, and ships plugins that lift request id and correlation id headers out of the request and into your logs. It is a small package with one dependency, and its 0.5 line is marked beta.

**tomwojcik/starlette-context** — Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id.

- Repository: https://github.com/tomwojcik/starlette-context
- Website: https://starlette-context.readthedocs.io/en/latest/
- Stars: 607 · Forks: 28
- Language: Python
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tomwojcik-starlette-context

## Request-scoped data that logging can reach

The README describes the package as middleware for Starlette that allows you to store and access the context data of a request, and says it can be used with logging so that logs automatically use request headers such as x-request-id or x-correlation-id.

That last sentence is the reason the package exists. In a request handling stack, the facts you most want in a log line, which request this is and which upstream call it belongs to, are also the facts most likely to be lost as a call goes deeper. Passing a request id down as a function argument through every layer is the kind of plumbing that decays, and a context object tied to the request removes the need for it.

The scope is small and worth stating clearly. The package is middleware, it operates on the request lifecycle, and the data it holds dies with the request. It is not a general context or configuration system, and it is not a background task or cache. The primary language is Python, the licence is MIT, and the documentation is on Read the Docs at starlette-context.readthedocs.io, with the changelog as a page within it.

The topics attached to the repository name fastapi as well as starlette, which tells you the intended consumer, since FastAPI is built on Starlette's middleware stack and a Starlette middleware drops into a FastAPI application unchanged.

## The middleware creates the context, the plugins fill it

The example in the README is short enough to read in one pass, and it shows the whole mechanism.

A handler accesses and stores data in the context and returns the context data as the response:

```python
async def index(request: Request):
    # Access and store data in context
    context["custom_value"] = "example"
    return JSONResponse(context.data)
```

The middleware is configured with a tuple of plugins:

```python
# Configure middleware
middleware = [
    Middleware(
        ContextMiddleware,
        plugins=(
            plugins.RequestIdPlugin(),
            plugins.CorrelationIdPlugin()
        )
    )
]
```

So the division of labour is between the middleware and the plugins. `ContextMiddleware` owns the lifetime of the context and creates it early in the request, and the plugins are what put values into it. The two bundled plugins read the request id and the correlation id, and the response in the example shows the result, with the two headers and the value the handler added:

```json
{
  "X-Correlation-ID": "5ca2f0b43115461bad07ccae5976a990",
  "X-Request-ID": "21f8d52208ec44948d152dc49a713fdd",
  "custom_value": "example"
}
```

The header names appear in the response because the plugins store them under the header name, which is why `context.data` is a plain mapping you can serialise straight into a response.

## Context stays writable for the whole request

One sentence in the README carries most of the design, and it is easy to skim past. Context can be updated and accessed at anytime if it's created in the middleware.

That conditional is doing the work. The context is created by the middleware, before the handler runs, and it is a mutable mapping that later code can both read and write. The `context` object in the example is imported from the package and used as if it were a plain dictionary, with `context["custom_value"] = "example"` on the left and `context.data` on the right.

The practical consequence is that anything running during the request, a dependency, a helper deeper in the stack, an exception handler, can reach the same context without it being passed as an argument. That is the mechanism that makes the logging use case work, because a log call in a function three layers down can pull the request id out of the context rather than out of a parameter list.

The limit is the same as the benefit. The context is per request and it is gone when the request ends, so anything that outlives the request, a background task, a message handler, a queued job, has to capture what it needs before the request returns. The README does not document any mechanism for that, so it is worth knowing before you depend on it.

## One dependency, and a wide Python range

The Dependencies section lists a single line, `starlette>=0.27.0`, and the installation is a pip install with Python 3.10 or newer:

```bash
# Python 3.10+
pip install starlette-context
```

The packaging metadata confirms the shape. The project requires Python 3.10 or newer, declares classifiers for Python 3.10 through 3.14, lists starlette as the only runtime dependency, and carries the keywords middleware, starlette and fastapi.

A single dependency is the right shape for middleware, because a middleware is something you put in the path of every request, and every dependency it drags in is a dependency your application inherits. Starlette itself is a reasonable exception, since you already have it.

The Python range is also worth noting for a package that is going to sit in the request path of a service. The classifiers run to 3.14, and the development status is 4 - Beta. A middleware is exactly the kind of component where a subtle change in how the framework dispatches requests can turn into a debugging session, so a package with a stated version range and a beta label deserves a look at its changelog before you pin it across a fleet.

## The example app is where the logging story is told

The repository root carries an `example/` directory, and its contents say more about the intended usage than the README does. There is an `app.py`, a `setup_logging.py`, a `requirements.txt`, a `mypy.ini`, a `Makefile` and a `README.md`.

A separate `setup_logging.py` is the tell. The point of the package is that logs pick up the request context, and that requires a logging setup that knows how to read it, so the example ships one rather than leaving you to guess. The dev dependency groups back this up, with an `example` group containing `structlog` and `uvicorn`, so the example is a structured logging setup rather than the standard library's.

Having the example as a directory rather than a snippet in the README means it can be run, type checked and linted like the rest of the project, with a mypy.ini of its own. For a package whose value is in how you wire it into a logging configuration, that is the part worth copying.

The README's own example is the minimal version of the same idea, with the app created by Starlette with routes and middleware, and run under uvicorn with `uvicorn.run(app, host="0.0.0.0")`.

## uv, pre-commit and a small Makefile for the developer

The development tooling is a small Makefile over uv, and it shows the shape of the project. The `init` target runs `uv sync`, and the `test` target syncs the dev group and then runs the test script:

```makefile
init:
	uv sync

test:
	uv sync --group dev
	sh scripts/test
```

The rest of the targets follow the same pattern. `run-hooks` syncs the code-quality group and runs pre-commit across all files, `clean` runs a script in `scripts/`, `docs` syncs the docs group and runs `make html` inside the docs directory, `update-deps` runs `uv lock --upgrade`, and `build` runs `uv build`.

The dependency groups are the interesting part of the manifest, because they are grouped by purpose rather than by kind. The dev group holds the test stack, with coverage, pytest, pytest-cov, pytest-sugar, pytest-xdist, pytest-asyncio, codecov and httpx. The docs group has sphinx, the sphinx-rtd-theme, toml and myst-parser. The example group has structlog and uvicorn. The code-quality group has ruff, mypy, docformatter, pyupgrade and pre-commit.

pytest-xdist for parallel runs and pytest-asyncio for the async tests are the two that tell you about the test suite, since both are only needed when the tests are numerous and the handlers are async, and the middleware under test is inherently async.

## Beta, on a 0.5 line, with a sponsor behind it

The maintenance picture is a small package on a fast-moving version line. The recent releases are v0.5.1 and v0.5.0, both on 2026-02-28, and v0.4.0 on 2025-03-04, and the repository is not archived with the last push on 2026-06-24.

The version scheme is worth reading carefully. A 0.5 release under semantic versioning means the public API is not considered stable, and the project says so itself with the Development Status 4 - Beta classifier. For a middleware that sits in the path of every request, that label is a reasonable thing for the maintainer to state and a reasonable thing for you to check before rolling it out widely.

There is a sponsor behind the work, and the README names them, with a thank you to Adverity for sponsoring the development of this OSS library. That is a meaningful signal for a package this small, because it explains how the maintenance happens at all.

The repository also carries a .pre-commit-config.yaml, an AUTHORS file, a .python-version file, a uv.lock, and a .codecov.yml, with the test suite badge pointing at a test-suite workflow and a separate coverage badge, so the package is checked by CI rather than by hand.

The resources section points at the source repository, the documentation and the changelog, and the contribution guide lives in the documentation rather than in the repository, which is where you should look before opening a pull request.

## Conclusion

starlette-context fits a Starlette or FastAPI service whose logs need a request identifier on every line and where passing that identifier down through call arguments has become awkward. It is the wrong choice if your service already has a logging setup that injects context, or if you need the context to survive past the request, since it is request-scoped by design. Verify first which headers your upstream actually sends, since the bundled plugins are built around request id and correlation id, and check the version you install, because the package is on a 0.5 line marked beta with a changelog on the documentation site.

## FAQ

### What does starlette-context do?

It is middleware for Starlette that lets you store and access the context data of a request. The documented use is with logging, so that log records automatically pick up request headers such as x-request-id or x-correlation-id.

### How do I install starlette-context?

With pip install starlette-context, on Python 3.10 or newer. The only runtime dependency is starlette at version 0.27.0 or above, and the package is published on PyPI as starlette-context.

### How do I add the request id and correlation id to the context?

Add the ContextMiddleware with a tuple of plugins, for example plugins.RequestIdPlugin() and plugins.CorrelationIdPlugin(), which read the corresponding headers and store them in the context. The response in the README example then contains X-Request-ID and X-Correlation-ID alongside any values the handler added.

### Can I change the context after the middleware has created it?

Yes. The README states that context can be updated and accessed at anytime if it is created in the middleware, so handlers and anything deeper in the stack can read from and write to the same per-request mapping.

### Is starlette-context ready for production use?

The packaging metadata classifies it as Development Status 4 - Beta, and the recent releases are v0.5.1 and v0.5.0 from 2026-02-28 after v0.4.0 from 2025-03-04. The changelog is published on the documentation site, and the development of the library was sponsored by Adverity.

## Sources

- [License: MIT](https://github.com/tomwojcik/starlette-context/blob/master/LICENSE)
- [Project website](https://starlette-context.readthedocs.io/en/latest/)
- [README](https://github.com/tomwojcik/starlette-context/blob/master/README.md)
- [Releases](https://github.com/tomwojcik/starlette-context/releases)
- [tomwojcik/starlette-context on GitHub](https://github.com/tomwojcik/starlette-context)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tomwojcik-starlette-context
