DEV Community

Cover image for FastAPI SQLAlchemy Session Leak Detection: Diagnose and Fix Long-Running DB Sessions in Production
Ayush Kumar
Ayush Kumar

Posted on • Originally published at logiclooptech.dev

FastAPI SQLAlchemy Session Leak Detection: Diagnose and Fix Long-Running DB Sessions in Production

You deploy your FastAPI application. Memory usage is stable. Latency is low. Two days later, your database CPU spikes to 100%. Your logs scream TimeoutError: QueuePool limit of size 5 overflow 10 reached. You restart the pods. Everything is fine... for another two days.

This is the classic signature of a SQLAlchemy Session Leak.

In this guide, we will move beyond basic "dependency injection" tutorials and dive into FastAPI session leak detection. We will analyze why SQLAlchemy long running sessions silently kill your app and how to implement robust SQLAlchemy connection management to fix it.

What a Session Leak Actually Looks Like in Production

A session leak is rarely a dramatic crash. It is a slow creep. Unlike a memory leak where RAM fills up, a session leak exhausts your Database Connection Pool.

The Symptoms:

  1. Gradual Latency Creep: Endpoints that usually take 50ms start taking 500ms, then 5s, then timeout.

  2. The "Cliff" Drop: The app works perfectly until it hits exactly X concurrent requests (usually pool_size + overflow), then it hangs completely.

  3. DB Metrics: Your database shows a high number of "Idle in Transaction" connections.

If you see these signs, you aren't running out of RAM. You are running out of slots in your FastAPI SQLAlchemy connection pool.

What Causes Long-Running SQLAlchemy Sessions?

A SQLAlchemy long running session occurs when a session is checked out from the pool but never returned. In standard Python scripts, this is obvious. In FastAPI's async environment, it is subtle.

The 3 Common Culprits:

1. The "Zombie" Background Task

You pass a DB session to a BackgroundTasks function, but you forget that the request context (and the dependency teardown) ends before the background task finishes.

Python

#WRONG
@app.post("/email")
async def send_email(db: Session = Depends(get_db)):
    # The 'db' session is closed when the request ends...
    # ...but this task tries to use it 5 seconds later!
    background_tasks.add_task(process_email, db) 
    return {"status": "sent"}
Enter fullscreen mode Exit fullscreen mode

2. Exception Swallowing

You manually create a session but fail to close it when an error occurs.

Python

#WRONG
def manual_task():
    db = SessionLocal()
    if check_something():
        raise Exception("Oops") # <--- Session never closed!
    db.close()
Enter fullscreen mode Exit fullscreen mode

3. Global Session Misuse

You accidentally instantiate a session at the module level (global scope) and reuse it across requests. This isn't just a leak; it causes data corruption.

How Connection Pools Hide Leaks (The Silent Problem)

To understand leaks, you must understand connection pooling in Python.

SQLAlchemy uses a QueuePool by default.

  • Pool Size: 5 (default).

  • Max Overflow: 10 (default).

When you call db = SessionLocal(), you aren't creating a TCP connection. You are borrowing one. When you call db.close(), you aren't closing the TCP connection. You are returning it.

If you forget to close(), the connection stays "Checked Out" forever. After 15 leaks, your 16th user gets a TimeoutError. The pool is empty, and the "leaked" connections are holding onto database resources doing absolutely nothing.

How to Detect Session Leaks in FastAPI

Finding these leaks in a codebase with 50 endpoints is hard. Here is how to implement FastAPI session leak detection.

1. Enable SQLAlchemy Pool Logging

The quickest way to see if you have a leak is to turn on debug logging for the pool.

Python

# config.py
engine = create_engine(DATABASE_URL, echo_pool="debug")
Enter fullscreen mode Exit fullscreen mode

What to look for in logs:

  • Healthy: Connection <...> checked out from pool followed immediately by Connection <...> being returned to pool.

  • Leak: You see checked out but never see returned.

2. Monitor "Checked Out" Count

You can programmatically check the status of your pool. Add this to your health check endpoint:

Python

@app.get("/debug/pool")
def pool_status():
    return {
        "checked_in": engine.pool.checkedin(),
        "checked_out": engine.pool.checkedout(),
        "overflow": engine.pool.overflow(),
        "size": engine.pool.size()
    }
Enter fullscreen mode Exit fullscreen mode

If checked_out keeps growing and never goes down, you have a leak.

How to Fix Leaks (Patterns That Actually Work)

Proper SQLAlchemy connection management relies on ensuring close() is called no matter what.

1. The Dependency Yield Pattern (The Gold Standard)

FastAPI's dependency injection system is designed to solve this. Use yield.

Python

#CORRECT
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        # This ALWAYS runs, even if the request crashes
        db.close()
Enter fullscreen mode Exit fullscreen mode

2. Fixing Background Tasks

Never pass the request's db session to a background task. Create a new, dedicated session scope inside the task.

Python

#CORRECT
def process_email_task(email_id: int):
    # Create a fresh session just for this background job
    with SessionLocal() as db:
        user = db.query(User).get(email_id)
        email_service.send(user)
    # Automatically closed here
Enter fullscreen mode Exit fullscreen mode

3. Implementing SQLAlchemy Retry Connection

Sometimes, connections die (network blip, DB restart). Your app should be resilient. Configure your engine with pool_pre_ping=True.

Python

engine = create_engine(
    DATABASE_URL,
    pool_pre_ping=True, # Checks if connection is alive before using it
    pool_recycle=3600   # Refreshes connections every hour
)
How to Monitor Session Leaks in Kubernetes / Docker
Enter fullscreen mode Exit fullscreen mode

If you run in production, looking at text logs isn't enough. You need metrics.

1. Export Connection Metrics

Use a sidecar or a middleware to push the engine.pool.checkedout() metric to Prometheus/Datadog every 15 seconds.

2. Set Alerts on "Pool Exhaustion"

Set an alert if: sqlalchemy_pool_checked_out > (pool_size * 0.8) for more than 5 minutes. This gives you a warning before users start getting 500 errors.

Conclusion

A SQLAlchemy long running session is a silent killer. It passes tests but crashes production. By respecting the yield pattern, isolating background tasks, and monitoring your pool size, you can make your FastAPI application bulletproof.

SEO Metadata

  • Title: FastAPI Session Leak Detection: Finding Long-Running SQLAlchemy Sessions

  • Description: Learn how to detect and fix FastAPI session leaks. A deep dive into SQLAlchemy connection pooling, long-running sessions, and production monitoring.

  • Tags: FastAPI, SQLAlchemy, Python, Database, Debugging

Top comments (0)