FastAPI Production Checklist: CORS, Rate Limiting, and Diagnostic Monitoring
FastAPI's default setup gets you from zero to a running API in minutes. But "running" and "production-ready" are different things. CORS defaults to same-origin (no header = no cross-origin), exception handlers print full tracebacks, and sync functions disguised as async endpoints silently block the event loop.
Here's the production checklist — the middleware, patterns, and diagnostics that prevent the three most common FastAPI production failures.
#CORS: why allow_origins=["*"] is a catastrophe
FastAPI's CORSMiddleware is explicit — you configure it or there's no CORS
header. That's safer than Express (which defaults to open). But the typical
StackOverflow answer gets it wrong:
# DON'T — catastrophic security risk
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
When allow_credentials=True, the browser sends cookies (including session
cookies) with cross-origin requests. If allow_origins=["*"], any website on
the internet can read authenticated responses from your API. That's not a
theoretical risk — it's a full account takeover for every user who visits a
malicious page while logged in.
The fix:
ALLOWED_ORIGINS = [
"https://your-domain.com",
"https://app.your-domain.com",
"http://localhost:3000", # dev only
]
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
)
The rule: explicit origin list, never wildcard with credentials. If you need
public API access without credentials, use allow_origins=["*"] but set
allow_credentials=False and document the tradeoff.
#Exception leakage: the traceback problem
FastAPI returns raw tracebacks when DEBUG=true (or when no debug flag is set
and an unhandled exception occurs). This leaks:
- File paths on your server (
/app/backend/routes/users.py:42) - Database connection strings (if they appear in the traceback)
- Library versions (useful for targeted exploits)
- Internal API structure
The fix — custom exception handler:
from fastapi import Request
from fastapi.responses import JSONResponse
import logging
logger = logging.getLogger("vergate.api")
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger.error(
"Unhandled exception",
exc_info=exc,
extra={"path": request.url.path, "method": request.method},
)
return JSONResponse(
status_code=500,
content={
"error": "Internal server error",
"detail": "An unexpected error occurred. Please try again.",
},
)
This logs the full traceback server-side (where you can see it in your logs) while returning a clean, generic error to the client.
Also add request logging middleware:
import time
import logging
logger = logging.getLogger("vergate.request")
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration_ms = (time.perf_counter() - start) * 1000
if response.status_code >= 500:
logger.error(
"%s %s %d %.1fms",
request.method, request.url.path,
response.status_code, duration_ms,
)
elif duration_ms > 1000:
logger.warning(
"%s %s %d %.1fms (slow)",
request.method, request.url.path,
response.status_code, duration_ms,
)
return response
The > 1000ms warning catches the async pitfall in the next section.
#The async trap: sync code inside async def
FastAPI runs all async def endpoints on a single event loop. If one
endpoint blocks (synchronous HTTP call, file I/O, CPU-bound work), every other
async request waits.
# DON'T — blocks the event loop
@app.get("/search")
async def search(q: str):
import requests
results = requests.get(f"https://api.example.com/search?q={q}") # BLOCKS
return {"results": results.json()}
This looks async. It's not. requests.get() is synchronous — it blocks the
entire event loop for the duration of the HTTP call. If that call takes 2
seconds, every other request waits 2 seconds.
The fix — use async HTTP clients:
import httpx
@app.get("/search")
async def search(q: str):
async with httpx.AsyncClient() as client:
results = await client.get(f"https://api.example.com/search?q={q}")
return {"results": results.json()}
Or use sync endpoints for blocking code:
# FastAPI runs sync endpoints in a threadpool — no event loop blocking
@app.get("/report")
def generate_report():
# Sync is fine here — FastAPI handles it correctly
data = expensive_computation()
return {"report": data}
The rule: async def = non-blocking only. def = blocking code that FastAPI
runs in a threadpool. Mixing them correctly is the difference between an API that
handles 1000 concurrent requests and one that falls over at 10.
#Rate limiting: the missing middleware
FastAPI has no built-in rate limiting. Without it, one abusive client can exhaust your database connections, memory, or API quotas.
Minimal rate limiter:
from collections import defaultdict
from time import time
request_counts: dict[str, list[float]] = defaultdict(list)
RATE_LIMIT = 100 # requests per window
WINDOW_SECONDS = 60
@app.middleware("http")
async def rate_limit(request: Request, call_next):
client_ip = request.client.host if request.client else "unknown"
now = time()
# Clean old entries
request_counts[client_ip] = [
t for t in request_counts[client_ip] if now - t < WINDOW_SECONDS
]
if len(request_counts[client_ip]) >= RATE_LIMIT:
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded. Try again later."},
)
request_counts[client_ip].append(now)
return await call_next(request)
For production, use a Redis-backed limiter (like slowapi) that works across
multiple worker processes.
#Structured logging: the diagnostic foundation
Production debugging without structured logs is guessing. Every log line should include request ID, path, method, status, duration, and user context.
import structlog
logger = structlog.get_logger("vergate")
@app.middleware("http")
async def add_request_context(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid4()))
bind_contextvars(request_id=request_id, path=request.url.path)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
#The Vergate scan: catch what you miss
Manual header inspection catches the obvious stuff. But Vergate's passive scanner
checks 20+ rules across CORS policies, security headers, exposed files, and
server information leakage — including route-specific header variations that
manual curl misses. Run it on your FastAPI backend at
vergate.dev/free-scan before every production
deploy. It takes 10 seconds and catches the middleware gaps that become incidents.
Frequently asked questions
Why is allow_origins=['*'] with credentials dangerous in FastAPI?
When Allow-Credentials is true, the browser sends cookies with cross-origin requests. If Allow-Origin is *, any website on the internet can read authenticated responses from your API — including session tokens, user data, and anything behind auth middleware. It's the single most common CORS misconfiguration we find.
How do I prevent FastAPI from leaking tracebacks in production?
Set DEBUG=false (or don't set it), register a custom exception handler that returns a generic error response, and never use ServerErrorMiddleware in production. FastAPI's default ExceptionHandler returns the traceback as a string when DEBUG mode is on — which it is in most development setups that get deployed accidentally.
What's the fastest way to detect blocking sync code in async endpoints?
Monitor endpoint p95 latency. If an async def endpoint shows latency spikes that correlate with CPU usage (not I/O), you likely have blocking sync code (file reads, requests.get(), synchronous DB calls) inside an async function. FastAPI runs async endpoints on a single event loop — one blocking call stalls every concurrent request.
Can Vergate detect FastAPI security issues?
Yes. Vergate's passive scanner checks CORS headers, security headers, server information leakage, exposed files, and technology fingerprinting on any web endpoint — including FastAPI backends.