FastAPI Middleware Tutorial
Learn how FastAPI middleware works under the hood and write your own for logging, timing, and request enrichment.
What you'll learn
- ✓What ASGI middleware is
- ✓Where it runs in the request cycle
- ✓Writing custom middleware
- ✓Ordering and pitfalls
- ✓When to choose dependencies instead
Prerequisites
- •Familiar with FastAPI basics and async Python
What and Why
Middleware in FastAPI is a function that wraps every request and response. It sits between the network and your route handlers. Anything that should apply to many endpoints, like adding a request ID, measuring latency, enforcing security headers, or compressing responses, belongs there.
FastAPI is built on Starlette, which is built on ASGI. So middleware in FastAPI is really ASGI middleware. Understanding that fact tells you why some things are easy at the middleware layer and others are awkward.
Mental Model
A request flows through layers in order. Each middleware gets the chance to inspect or modify the request, call the next layer, then inspect or modify the response on the way out. The pattern is an onion: outermost middleware sees the raw HTTP first and the final response last.
Two important details. First, middleware runs for every request, including those that 404 because no route matched. Second, middleware does not have access to FastAPI’s dependency injection results. If you need access to a logged-in user, that is a job for dependencies, not middleware.
Hands-on Example
Here is a simple middleware that adds a request ID header and logs the response time.
import time
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
request.state.request_id = request_id
start = time.perf_counter()
response = await call_next(request)
elapsed_ms = (time.perf_counter() - start) * 1000
response.headers["x-request-id"] = request_id
response.headers["x-response-time-ms"] = f"{elapsed_ms:.1f}"
return response
A handler can read the request ID via request.state.
@app.get("/orders")
async def list_orders(request: Request):
rid = request.state.request_id
return {"request_id": rid, "items": []}
For middleware that needs configuration or class state, write it as an ASGI class.
from starlette.types import ASGIApp, Receive, Scope, Send
class SecurityHeadersMiddleware:
def __init__(self, app: ASGIApp, hsts: bool = True):
self.app = app
self.hsts = hsts
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
return await self.app(scope, receive, send)
async def send_wrapper(message):
if message["type"] == "http.response.start":
headers = list(message.get("headers", []))
headers.append((b"x-content-type-options", b"nosniff"))
if self.hsts:
headers.append((b"strict-transport-security", b"max-age=31536000"))
message["headers"] = headers
await send(message)
await self.app(scope, receive, send_wrapper)
app.add_middleware(SecurityHeadersMiddleware, hsts=True)
Network -> SecurityHeadersMiddleware
|
v
RequestIdMiddleware
|
v
Router -> Dependencies -> Handler
|
v
Response on the way back through each middleware The order matters. Middleware added last runs first on the way in and last on the way out. CORS, for example, almost always belongs near the outermost layer.
Common Pitfalls
- Reading the request body in middleware. Once you consume
await request.body(), the handler will not see it unless you re-inject it. For body inspection, prefer a custom route class or dependency. - Using sync I/O. Middleware runs in the event loop. A blocking call freezes the whole worker. Use async clients only.
- Catching exceptions silently. If your middleware swallows errors, you lose 500 logs and the exception handlers never see them.
- Confusing middleware with dependencies. Dependencies have access to typed parameters, security scopes, and DI. Middleware only sees the raw request.
- Adding too many middlewares. Every layer adds latency, especially for small JSON responses. Combine related concerns.
Practical Tips
- Use
request.stateto pass data from middleware to handlers. It is a per-request namespace. - Add a request ID at the very outside. Every log line downstream can include it for tracing.
- Prefer Starlette’s built-in
CORSMiddleware,GZipMiddleware, andTrustedHostMiddlewarewhen they fit. - For authentication, use a dependency. It is reusable, typed, and integrates with OpenAPI security schemes.
- Benchmark with and without each middleware. You will be surprised what costs the most.
Wrap-up
Middleware gives you a single place to apply cross-cutting concerns to every request. Use it for request IDs, timing, security headers, and compression. Reach for dependencies when you need typed parameters or per-route control. Once you understand the ASGI signature underneath, you can write middleware that is fast, predictable, and easy to test.
Related articles
- FastAPI FastAPI Authentication with JWT
Implement JWT-based authentication in FastAPI with OAuth2 password flow, secure token signing, and a reusable get_current_user dependency.
- FastAPI FastAPI CORS: A Practical Tutorial
Configure CORS in FastAPI without security holes: how the browser preflight works, which origins and headers to allow, credentials and cookies, and the most common misconfigurations to avoid.
- FastAPI FastAPI Deployment with Uvicorn and Gunicorn
Deploy FastAPI to production with Gunicorn managing Uvicorn workers. Cover process counts, timeouts, and health checks.
- FastAPI FastAPI OpenAPI Customization: A Practical Tutorial
Tailor FastAPI's auto-generated OpenAPI schema: tags, summaries, examples, response models, custom operation IDs, security schemes, and a custom Swagger UI your team will actually use.