This article highlights two critical production issues faced when deploying FastAPI applications: inefficient database connection management and insecure error handling. It explains how to correctly implement database connection pooling using FastAPI's lifespan events and how to establish a global exception handler to prevent sensitive information leaks, thereby improving both performance and security.
Read original on Dev.to #systemdesignFramework quickstarts often prioritize ease of use over production best practices, leading to performance bottlenecks and security vulnerabilities when applications scale. This article illustrates common pitfalls with FastAPI, specifically regarding database connection management and exception handling, which are critical for robust system design.
A common anti-pattern in early FastAPI development is establishing a new database connection for every incoming request. This leads to significant overhead from repeated TCP handshakes and authentication, quickly exhausting the database's connection limits under moderate load, and resulting in high P99 latencies.
# ❌ What most people write first
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
conn = await asyncpg.connect(DATABASE_URL) # new connection every request
order = await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)
await conn.close()
return orderThe recommended solution involves using a connection pool initialized once at application startup and properly closed on shutdown. FastAPI's `lifespan` event handler is the ideal place for this, ensuring shared resources like database connections (or Redis clients, HTTP sessions) are managed efficiently across all requests, preventing connection storms and improving overall application throughput.
# ✅ The right way
from contextlib import asynccontextmanager
from fastapi import FastAPI
import asyncpg
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.db = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=10)
yield
await app.state.db.close()
app = FastAPI(lifespan=lifespan)
@app.get("/orders/{order_id}")
async def get_order(order_id: str, request: Request):
async with request.app.state.db.acquire() as conn:
return await conn.fetchrow("SELECT * FROM orders WHERE id = $1", order_id)By default, unhandled exceptions in FastAPI can expose sensitive details like internal file paths, dependency names, and full stack traces to clients. This is a significant security vulnerability. Implementing a global exception handler allows logging the full traceback internally for debugging while presenting a generic, safe error message to the client.
# ✅ Global exception handler
import logging
from fastapi import Request
from fastapi.responses import JSONResponse
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.exception(
"Unhandled exception",
extra={"path": request.url.path, "method": request.method}
)
return JSONResponse(
status_code=500,
content={
"error": "internal_server_error",
"message": "Something went wrong."
}
)Observability Best Practice
Additionally, adopting structured logging (e.g., JSON format) is crucial for production. This makes logs easily parsable by centralized logging systems like Cloud Logging, improving observability and incident response.