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.