Managing Startup and Shutdown Resources in FastAPI with the Lifespan Event Handler
Learn how FastAPI’s lifespan event handler centralises startup and shutdown logic, with a worked example using a SQLAlchemy async connection pool and notes on version and server requirements.
08 Oct 2026, 01:22 UTC

The Problem: Scattered Setup Code
When a FastAPI application needs to initialize external resources—such as a database connection pool, a message‑broker client, or a cache—developers often scatter the setup and teardown logic across multiple @app.on_event('startup') and @app.on_event('shutdown') decorators. As the project grows, it becomes easy to forget a cleanup step, to duplicate initialization, or to end up with conflicting order when both decorators and manual calls coexist.
The Solution: Using FastAPI's Lifespan Parameter
FastAPI 0.95.0 introduced the lifespan argument, which accepts an async context manager. Anything placed before the yield statement runs when the ASGI server starts up; anything after the yield runs when the server shuts down. This centralises resource management and guarantees that the shutdown code executes exactly once, provided the server implements the ASGI lifespan protocol (Uvicorn ≥ 0.14, Hypercorn, etc.).
Worked Example: Database Connection Pool
Below is a minimal main.py that creates a SQLAlchemy async engine at startup and disposes of it at shutdown.
# main.py
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
# Replace with your actual database URL
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
async def lifespan(app: FastAPI):
# ----- startup -----
engine: AsyncEngine = create_async_engine(DATABASE_URL, echo=False)
# make the engine available to routes via app.state
app.state.db_engine = engine
# (you could also run migrations here)
yield
# ----- shutdown -----
await engine.dispose()
app = FastAPI(lifespan=lifespan)
@app.get("/items/")
async def read_items():
# Example usage: acquire a connection from the pool
async with app.state.db_engine.connect() as conn:
result = await conn.execute("SELECT 1")
return {"result": result.scalar()}
To run the example:
- Install FastAPI and Uvicorn:
pip install fastapi uvicorn sqlalchemy asyncpg - Start the server:
uvicorn main:app --reload - Open http://localhost:8000/docs to see the Swagger UI.
- When the server starts, you should see the Uvicorn startup log followed by any print statements you add inside the
lifespanblock (if you add them). When you stop the server with Ctrl+C, the shutdown log appears after the request‑handling logs.
Notice that the OpenAPI documentation is still served correctly; the lifespan handler does not interfere with FastAPI’s routing or schema generation.
Trade‑offs and Limitations
- ASGI server requirement: The lifespan protocol is ignored by servers that do not implement it (e.g., older versions of Uvicorn or non‑ASGI WSGI servers). If you deploy to such a server, the startup/shutdown code will never run, leaving resources uninitialized.
- Version dependency: You need FastAPI ≥ 0.95.0. Check with
pip show fastapi; older versions will raise aTypeErrorwhen passinglifespantoFastAPI(). - Mixing with
@app.on_event: While FastAPI still supports the deprecated startup/shutdown decorators, using them together withlifespancan lead to confusing execution order (allon_eventcallbacks run before the lifespan startup, and after the lifespan shutdown). For clarity, pick one approach and stick with it across the codebase. - Error handling: If an exception occurs before the
yield, the server will start but the shutdown code will still run. Ensure any critical initialization errors are caught and logged so you can decide whether to abort startup.
Putting It Into Practice
Adopt the lifespan pattern when you need reliable, deterministic setup and teardown of external resources. Begin by auditing existing @app.on_event handlers, migrate them into a single async context manager, and verify the server logs show the expected startup and shutdown messages. Keep the lifespan function focused: create resources, assign them to app.state (or a dependency), yield, then clean up. This keeps your application’s entry point readable and reduces the risk of leaked connections or unclosed clients.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.