A practical guide to implementing middleware in FastAPI for better performance, security, and efficiency
In this article, we'll explore how to add middleware to a FastAPI application. We'll go through three examples: adding a process time header to track how long requests take, setting up security headers to keep your app safe, and using GzipMiddleware to compress responses for faster delivery. Plus, we'll talk about why the order of these middlewares matters.
Middleware in FastAPI is a function that runs for every request and response. It takes the incoming request, does something with it (or not), passes it to your app, and then takes the response, does something again (or not), and sends it back. Simple, right? It's super useful for tasks that need to happen globally, like adding headers or compressing data.
In FastAPI, you create middleware with the @app.middleware("http")
decorator or use built-in options like GZipMiddleware. Let's jump into our examples.
First, let's add a middleware that tracks how long it takes to process a request. This is great for spotting slowdowns or debugging.
Here's how it works: when a request hits, we mark the start time. Then, we let the app handle it and get a response. Once that's done, we check the time again, calculate the difference, and add it as a header called X-Process-Time.
Here's the code:
import time from fastapi import FastAPI, Request app = FastAPI() @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.perf_counter() response = await call_next(request) process_time = time.perf_counter() - start_time response.headers["X-Process-Time"] = str(process_time) return response
We use time.perf_counter()
because it's more precise than time.time()
for short measurements. One thing to note: if you want browsers to see this header, you might need to adjust your CORS settings with expose_headers
. But that's optional and depends on your setup.
Next up, let's make your app safer with security headers. These tell browsers how to handle your site and can block common attacks like clickjacking or cross-site scripting.
Here's a middleware that adds them:
from fastapi import Request async def add_security_headers(request: Request, call_next): response = await call_next(request) response.headers["X-Frame-Options"] = "DENY" response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains" response.headers["Permissions-Policy"] = ( "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=()," "magnetometer=(),gyroscope=(),fullscreen=(self),payment=()" ) response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Content-Security-Policy"] = ( "default-src 'self'; " "script-src 'self' 'unsafe-inline' data: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://www.google.com https://www.gstatic.com https://z.clarity.ms https://*.clarity.ms https://*.googlesyndication.com; " "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdnjs.cloudflare.com; " "img-src 'self' data: https://lh3.googleusercontent.com https://*.twimg.com https://*.fbcdn.net https://*.ytimg.com https://*.fna.fbcdn.net https://*.clarity.ms https://*.tiktokcdn.com https://*.tiktokcdn-us.com image/; " "font-src 'self' data: https://fonts.gstatic.com; " "object-src 'none'; " "form-action 'self'; " "frame-src 'self' https://www.google.com https://www.gstatic.com; " "connect-src 'self' https://*.clarity.ms https://z.clarity.ms; " "upgrade-insecure-requests" ) return response
That's a lot, so let's break it down:
X-Frame-Options: DENY
: Stops your site from loading in an iframe, blocking clickjacking.X-Content-Type-Options: nosniff
: Prevents browsers from guessing file types, reducing some attack risks.Strict-Transport-Security
: Forces HTTPS for two years (max-age=63072000), keeping connections secure.Content-Security-Policy
: Controls where scripts, styles, and images can come from, like a whitelist for your app.You might need to tweak these based on your app - say, if you use external scripts or images, adjust the CSP. But this is a solid base.
Next, let's lock down which domains can access your API using TrustedHostMiddleware
. This is a crucial security step to prevent Host Header attacks.
Why It's Essential: A malicious actor could send a request with a fake Host
header. If your application uses this header to generate URLs (like in password reset emails), it could be tricked into sending users to a malicious site. TrustedHostMiddleware
validates the Host
header against a list of allowed domains and will raise an error if it doesn't match.
Here's how to add it:
from fastapi import FastAPI from starlette.middleware.trustedhost import TrustedHostMiddleware app = FastAPI() app.add_middleware( TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"] )
Make sure to replace example.com
with your actual domain. You can use wildcards for subdomains.
If your frontend is served from a different domain than your backend (like localhost:3000
for a React dev server and localhost:8000
for FastAPI), you'll run into Cross-Origin Resource Sharing (CORS) issues. Browsers block these requests by default for security.
FastAPI's CORSMiddleware
makes it easy to tell browsers which origins, methods, and headers are allowed.
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "http://localhost:3000", "https://your-frontend-domain.com", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
allow_origins
: A list of domains that are allowed to make requests.allow_credentials
: Allows cookies to be included in cross-origin requests.allow_methods
and allow_headers
: Use ["*"]
to allow all standard methods and headers, or specify them for tighter security.For a more detailed example of using CORS when serving a frontend application, see my guide on Serving a React Frontend Application with FastAPI.
Finally, let's speed things up with compression. The GZipMiddleware compresses responses if the client says it's cool with GZip (via the Accept-Encoding: gzip
header). Smaller responses mean faster load times.
Here's how to use it:
from fastapi import FastAPI from fastapi.middleware.gzip import GZipMiddleware app = FastAPI() app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5)
The minimum_size=1000
means it only compresses responses over 1000 bytes - smaller ones aren't worth the effort. The compresslevel=5
is a middle ground: 1 is fast but less compression, 9 is slow but shrinks more. Five works well for most cases.
When a client asks for GZip, this middleware compresses the response body and adds Content-Encoding: gzip
. It's a quick win for performance, especially on slow networks.
What happens when an unexpected error occurs? By default, FastAPI returns a generic 500 Internal Server Error
. A custom exception middleware lets you catch these errors, log them, and return a clean JSON response.
import logging from fastapi import FastAPI, Request from starlette.responses import JSONResponse app = FastAPI() logger = logging.getLogger(__name__) @app.middleware("http") async def exception_handler_middleware(request: Request, call_next): try: return await call_next(request) except Exception as e: # Log the exception for debugging logger.error(f"Unhandled exception: {e}", exc_info=True) # Return a standardized error response return JSONResponse( status_code=500, content={"message": "An internal server error occurred."}, )
Important: This middleware should be placed very early in the chain (but after CORS) to catch errors from other middlewares as well.
Now, how do we put these together? And does the order matter?
Yes, it does.
In FastAPI (and Starlette, which it's built on), middlewares run in the order you add them for requests and reverse order for responses. So, the first one added is the first to see the request and the last to touch the response.
It's crucial to add CORSMiddleware
first, before any other middleware. This ensures that it can properly handle CORS preflight requests and add the necessary headers to all responses, including errors generated by other middleware. Security middleware like TrustedHostMiddleware
should come next.
Imagine this:
Host
header.Here's how to set it up:
from fastapi import FastAPI from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.trustedhost import TrustedHostMiddleware from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware app = FastAPI() # CORS middleware first! app.add_middleware( CORSMiddleware, allow_origins=["*"], # Adjust for production allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Then Trusted Host app.add_middleware( TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"] ) # Then other middlewares app.add_middleware(BaseHTTPMiddleware, dispatch=add_security_headers) # Then process time app.add_middleware(BaseHTTPMiddleware, dispatch=add_process_time_header) # GZip last app.add_middleware(GZipMiddleware, minimum_size=1000)
Notice we use BaseHTTPMiddleware
for custom functions - it's a wrapper FastAPI needs. Built-in ones like CORSMiddleware
, TrustedHostMiddleware
, and GZipMiddleware
go straight in.
This order works because security and process time only add headers, and GZip compresses the body. Headers aren't compressed, so adding them after GZip is fine. But if a middleware needed to read the response body, you'd put it before GZip to see the uncompressed version.
Think about what each middleware does and plan the sequence.
Middleware is more than just a technical detail in FastAPI; it's what elevates a simple API into a secure, resilient, and production-grade service. While FastAPI gives you the tools to build fast endpoints, the right middleware stack provides the security, performance, and observability needed to run them confidently in the wild.
We've covered how to:
TrustedHostMiddleware
and strong security headers.CORSMiddleware
.GZipMiddleware
.Think of this middleware stack as a solid foundation for any serious FastAPI project. By understanding how these components work together - and why their order matters - you're not just adding features. You're building a better, safer, and more efficient application.
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Enjoyed this blog post? Check out these related posts!
6 Essential FastAPI Middlewares for Production-Ready Apps
A guide to the 6 key middlewares for building secure, performant, and resilient FastAPI applications.
Read More..
FastAPI Tutorial: A Complete Guide for Beginners
A step-by-step guide to building your first API with Python and FastAPI, from installation to production-ready concepts.
Read More..
Simple CI/CD for Your FastAPI App with Google Cloud Build and Cloud Run
Push code, deploy automatically: A simple CI/CD guide for your web app.
Read More..
Running Database Migrations with Alembic in Google Cloud Build
How to Organize and Load FastAPI Settings from a .env File Using Pydantic v2
Read More..
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.