Async Dependency Lifecycles in FastAPI: When Generators Go Wrong
Do you ever see “Connection pool exhausted” after a burst of requests?
⚠️ Async generator deps can leak resources if not closed; use asynccontextmanager + Depends for clean, per‑request lifecycles.
🎯 System Scenario & Requirements
System Goal
⚠️ Async generator deps can leak resources if not closed; use asynccontextmanager + Depends for clean, per‑request lifecycles.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- ⚠️ Async generator deps can leak resources if not closed; use asynccontextmanager + Depends for clean, per‑request lifecycles.
- In a FastAPI app, it’s tempting to write a dependency that yields a DB session via an async generator: If the dependency is used in many routes, the Session objects pile up because FastAPI only closes the generator when the request ends and the generator’s finally block runs.
- Forgetting that block means connections stay open, eventually exhausting the pool.
Constraints & Assumptions
- Operational latency, consistency levels, and network partition boundaries must be enforced.
⚡ Execution Stages & Decision Trace
Stage 1 — The Solution: `asynccontextmanager` + `Depends`
Wrap the generator in asynccontextmanager from contextlib .
FastAPI automatically calls the cleanup when the request finishes: FastAPI injects the yielded session and ensures the finally block runs even if the endpoint raises.
Deep Dive
Deep Dive: Stage 1 — The Solution: asynccontextmanager + Depends
Wrap the generator in asynccontextmanager from contextlib. FastAPI automatically calls the cleanup when the request finishes:
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI
app = FastAPI()
@asynccontextmanager
async def get_session():
session = Session()
try:
yield session
finally:
await session.close() # guaranteed cleanup
@app.get("/items")
async def read_items(session: Session = Depends(get_session)):
return await session.execute("SELECT * FROM items")
FastAPI injects the yielded session and ensures the finally block runs even if the endpoint raises. This pattern scales: each request gets its own session, and resources are freed promptly.
Observable outcome: This pattern scales: each request gets its own session, and resources are freed promptly.
Deep Dive: Stage 2 — Trade‑offs & Best Practices
| Aspect | Benefit | Caveat |
| Per‑request generator | Isolation, no shared state | Slight overhead of creating a session per call |
asynccontextmanager | Automatic cleanup | Must await close; cannot reuse across requests |
| Dependency scopes | Depends(get_session, use_cache=False) | Forces new instance per request; default caching can be a source of leaks if misused |
Quick checklist
- Use
asynccontextmanagerfor any async resource (DB, HTTP client, file). - Explicitly
awaitcleanup in thefinallyblock. - Avoid caching async generators unless you manage lifecycle yourself.
- Monitor connection pool metrics; a sudden spike indicates a leak.
By treating async generators as true context managers, you keep your FastAPI app lean, responsive, and free of resource leaks.
Observable outcome: By treating async generators as true context managers, you keep your FastAPI app lean, responsive, and free of resource leaks.
📊 System Outcome & Trade-offs
Observed User & Operator Behavior
User: Clients observe deterministic state without lost updates or race conditions.
Operator: Operators can verify stage outcomes against the scenario requirements.
Key Architectural Trade-offs
- System guarantees are traded against replication overhead and failover convergence times.
Key learnings
- Wrap the generator in asynccontextmanager from contextlib .
- Quick checklist - Use asynccontextmanager for any async resource (DB, HTTP client, file).
Apply Concepts from this Scenario
Test your understanding of the architecture decisions and trade-offs presented in this case study.
