Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

Unit 12.00: The database session, per request

One database session per request, always closed, even when the handler raises.

Yield, then finally

A session dependency using yield, exercised over three requests.

The code confirms every session opened was closed.

from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient

OPENED, CLOSED = [], []


class Session:
    def __init__(self, n): self.n = n
    def query(self): return f"rows from session {self.n}"


def get_session():
    session = Session(len(OPENED) + 1)
    OPENED.append(session.n)
    try:
        yield session
    finally:
        CLOSED.append(session.n)


app = FastAPI()


@app.get("/rows")
def rows(session: Session = Depends(get_session)) -> dict:
    return {"data": session.query()}


client = TestClient(app)
for _ in range(3):
    client.get("/rows")
print(f"opened: {OPENED}")
print(f"closed: {CLOSED}")
print(f"all closed: {OPENED == CLOSED}")

# `yield` plus `finally` is the shape: one session per request, always closed,
# even when the handler raises. A session shared across requests leaks state
# between callers and eventually deadlocks.

The finally runs whether the handler returned or raised, which is what makes the guarantee hold. A session leaked on the error path exhausts the pool slowly, so the symptom appears hours later and under load.

One session per request is the other half. A session shared across requests leaks state between callers and eventually deadlocks.

The mistake this prevents

The mistake is opening a session at module level and reusing it. It works in development with one request at a time, and fails under concurrency in ways that look like data corruption.

Takeaway

Use a yield dependency with finally for sessions. One per request, always closed - a leak on the error path surfaces as exhaustion much later.