FastAPI Resource Management: Choosing Lifespan over on_event
Learn why the FastAPI lifespan context manager is the preferred choice over deprecated on_event handlers for managing async startup and shutdown resources.
16 Jul 2025, 14:24 UTC

The Problem: Deterministic Resource Lifecycle
FastAPI applications often require shared resources—such as database connection pools, Redis clients, or HTTP session managers—that must be initialized once at startup and closed cleanly when the server stops. Without a deterministic lifecycle, applications risk leaking connections or failing to flush buffers during a shutdown, leading to unstable environments and difficult-to-debug production errors.
Useful Takeaway: For all new development and migrations in current FastAPI versions, use the lifespan context manager. Avoid app.add_event_handler("startup", ...) and "shutdown", as these legacy on_event handlers are deprecated and may be removed in future releases.
Decision Constraints
- Deterministic Cleanup: Shutdown logic must execute even if the application encounters an error during its runtime.
- Async Initialization: Startup processes must support
awaitto prevent blocking the event loop during server boot. - ASGI Compatibility: The mechanism must be recognized by ASGI servers like Uvicorn or Hypercorn to trigger the lifecycle events correctly.
- Testability: Resources should be manageable within a test suite without relying on global state that persists between test cases.
Comparing Lifecycle Options
| Feature | Lifespan Context Manager | on_event Handlers (Legacy) |
|---|---|---|
| API Pattern | async def lifespan(app: FastAPI): yield |
@app.on_event("startup") |
| Logic Structure | Centralized (Start $\rightarrow$ Yield $\rightarrow$ Stop) | Decoupled (Two separate functions) |
| Async Support | Native async/await |
Supported via async callbacks |
| Status | Recommended | Deprecated |
| Testability | High (via TestClient context) | Moderate (requires manual trigger) |
Trade-offs and Engineering Considerations
The lifespan approach centralizes the resource lifecycle into a single block of code. Everything before the yield statement is the startup phase, and everything after is the shutdown phase. This ensures that the shutdown logic is logically paired with the startup logic, reducing the risk of forgetting to close a resource that was opened.
In contrast, on_event splits these concerns into separate functions. In complex applications with multiple handlers, the execution order can become implicit and harder to verify, which complicates dependency management between different shared resources.
One limitation of lifespan is that it requires Python 3.7+ and a strict adherence to the async generator pattern. Additionally, state stored on app.state is local to the process; in multi-worker deployments (e.g., Uvicorn with --workers 4), each worker process executes its own lifespan and maintains its own separate resource pool.
Implementation Pattern
To implement a lifespan manager, define an async generator that initializes resources, attaches them to the app.state for access in request handlers, and cleans them up after the yield.
from fastapi import FastAPI, Request
from typing import AsyncGenerator
from contextlib import asynccontextmanager
# Define the lifespan logic
@asynccontextmanager
async def lifespan(app: FastAPI):
# --- Startup ---
# Initialize a simulated database pool
print("Initializing database pool...")
app.state.db_pool = "Connected to DB"
app.state.is_ready = True
yield # The application runs while paused here
# --- Shutdown ---
print("Closing database pool...")
app.state.is_ready = False
# await app.state.db_pool.close()
# Pass the lifespan manager to the FastAPI constructor
app = FastAPI(lifespan=lifespan)
@app.get("/status")
async def get_status(request: Request):
# Access resources via request.app.state
return {"ready": request.app.state.is_ready, "db": request.app.state.db_pool}
Execution: Run this application using an ASGI server from your terminal. No special permissions are required beyond those needed to run Python.
uvicorn main:app --reload
Risk Warning: Do not perform long-running blocking I/O (like synchronous time.sleep() or blocking socket calls) inside the lifespan function. This will delay the server's ability to start accepting requests. Use await with async libraries or offload blocking calls to a thread pool.
Validation and Verification
To verify that the lifecycle is functioning correctly without manually restarting the server, use the TestClient as a context manager. This triggers the lifespan startup on entry and shutdown on exit.
from fastapi.testclient import TestClient
# Use the app defined in the implementation section
client = TestClient(app)
# The 'with' block triggers the lifespan startup
with client:
response = client.get("/status")
assert response.json()["ready"] is True
# After the block exits, the lifespan shutdown has executed
# (Verification can be done by checking external logs or state flags)
Expected Result: The /status endpoint should return ready: True while the client is active. Once the context manager exits, the shutdown print statement should appear in the console.
Version Check: Ensure you are using a recent version of FastAPI to avoid compatibility issues with the lifespan parameter.
pip show fastapi
Rollback and Migration
If you are migrating from on_event to lifespan and encounter initialization order issues, you can revert by removing the lifespan parameter from the FastAPI constructor and reinstating the @app.on_event("startup") decorators. However, this should be a temporary measure as the legacy handlers are deprecated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.