Introduction
FastAPI provides a powerful dependency injection system that allows efficient request handling and resource management. However, improper use of dependencies, such as creating database connections per request, failing to manage in-memory caches, or keeping global objects unnecessarily alive, can lead to performance degradation and memory leaks. These issues become particularly problematic in high-concurrency environments where resource allocation needs to be optimized. This article explores common pitfalls of dependency management in FastAPI, debugging techniques, and best practices for improving API performance.
Common Causes of Performance Bottlenecks and Memory Leaks
1. Creating a New Database Connection Per Request
Opening a new database connection for every request significantly increases latency and resource consumption.
Problematic Scenario
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app = FastAPI()
@app.get("/items")
def read_items(db=Depends(get_db)):
return db.execute("SELECT * FROM items").fetchall()
This setup creates and closes a new database session for every request, which can lead to connection pool exhaustion in high-traffic applications.
Solution: Use a Singleton Database Session
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from database import get_db
db_session = get_db()
app = FastAPI()
@app.get("/items")
def read_items(db: Session = Depends(db_session)):
return db.execute("SELECT * FROM items").fetchall()
Reusing a single database session improves connection pooling efficiency and reduces latency.
2. Unnecessary In-Memory Object Retention Leading to Memory Leaks
Storing large objects in global variables without proper cleanup leads to memory retention.
Problematic Scenario
cache = {}
@app.get("/cache/{key}")
def get_cache(key: str):
return cache.get(key, "Not found")
Over time, this in-memory cache will grow indefinitely, consuming memory.
Solution: Use a TTL-Based Cache
from cachetools import TTLCache
cache = TTLCache(maxsize=100, ttl=300) # Automatically expires after 5 minutes
Using a TTL-based cache prevents unbounded memory growth.
3. Dependency Execution on Every Request Instead of Using a Singleton
FastAPI’s dependency system re-initializes dependencies on each request if not scoped properly.
Problematic Scenario
def expensive_dependency():
time.sleep(2) # Simulating heavy computation
return "Computed Value"
@app.get("/compute")
def compute(value=Depends(expensive_dependency)):
return {"value": value}
This function runs on every request, causing delays.
Solution: Cache Expensive Dependencies
from functools import lru_cache
@lru_cache()
def expensive_dependency():
time.sleep(2)
return "Computed Value"
Caching prevents repeated computation.
4. Blocking Operations in Async Routes Causing Deadlocks
Using synchronous database queries inside an async FastAPI route causes request queue congestion.
Problematic Scenario
@app.get("/users")
async def get_users():
return db.execute("SELECT * FROM users").fetchall()
This blocks the event loop and slows down request handling.
Solution: Use `async` Database Queries
@app.get("/users")
async def get_users():
async with async_session() as session:
result = await session.execute("SELECT * FROM users")
return result.fetchall()
Using an async database session prevents blocking the event loop.
5. Overuse of Global State in Dependencies
Storing large objects globally increases memory usage unnecessarily.
Problematic Scenario
global_config = {}
def get_config():
return global_config
Solution: Store Configuration in Environment Variables
import os
config_value = os.getenv("CONFIG_VALUE")
Using environment variables avoids unnecessary memory retention.
Best Practices for Efficient Dependency Management in FastAPI
1. Reuse Database Sessions Instead of Creating New Ones Per Request
Prevent connection pool exhaustion by maintaining a shared session.
Example:
db_session = get_db()
2. Use Caching to Prevent Repeated Expensive Computations
Reduce execution overhead with `lru_cache`.
Example:
@lru_cache()
def expensive_function():
3. Avoid Global Object Retention to Prevent Memory Leaks
Use TTL-based caching for temporary storage.
Example:
cache = TTLCache(maxsize=100, ttl=300)
4. Use Async Database Queries to Prevent Blocking
Optimize database operations with async execution.
Example:
async with async_session() as session:
5. Store Configuration in Environment Variables Instead of Global State
Avoid unnecessary memory usage.
Example:
os.getenv("CONFIG_VALUE")
Conclusion
Performance bottlenecks and memory leaks in FastAPI often result from improper dependency management, excessive database connections, unnecessary object retention, and blocking synchronous operations. By optimizing dependency lifecycles, implementing caching strategies, using async database queries, and managing configuration efficiently, developers can maintain high-performance FastAPI applications. Regular profiling with tools like `async-profiler` and monitoring database connections help identify and resolve performance issues in production environments.