FastAPI Lifespan and app.state: Managing Process-Wide Resources
Use FastAPI's lifespan context manager to create and dispose shared clients or pools once at startup and shutdown, expose them via app.state, and keep request-scoped cleanup in dependencies with yield.
11 Mar 2026, 08:19 UTC

The problem: where shared resources belong
When a FastAPI application needs a database connection pool, an async HTTP client, or an in-memory cache, the question is where to create it and where to close it. Creating a new client inside every endpoint wastes connections and slows requests. Creating it at module import time makes testing and graceful shutdown harder. The practical answer is FastAPI's lifespan context manager: build the resource before the application serves traffic, attach it to app.state, and dispose of it after the yield in a finally block.
How lifespan works in the ASGI lifecycle
FastAPI runs lifespan through the ASGI lifespan protocol. Code before yield executes during server startup. The application stays suspended at yield while it serves requests. Code after yield runs during shutdown. Handlers access shared objects through request.app.state. This is process-wide state: every request in that worker sees the same object.
A worked example: a process-wide pool on app.state
The following example creates a simple pool once at startup, exposes it on app.state, and closes it at shutdown.
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
class Pool:
def __init__(self):
self.is_open = False
async def start(self):
self.is_open = True
async def stop(self):
self.is_open = False
@asynccontextmanager
async def lifespan(app: FastAPI):
pool = Pool()
await pool.start()
app.state.pool = pool
try:
yield
finally:
await pool.stop()
app = FastAPI(lifespan=lifespan)
@app.get("/ready")
async def ready(request: Request):
return {"pool_open": request.app.state.pool.is_open}
The pool is created once when the ASGI server starts. The endpoint reads it from app.state. After yield, the finally block ensures stop() runs even if an exception occurs during shutdown.
Running and checking the example
Use TestClient as a context manager so the lifespan events fire:
from fastapi.testclient import TestClient
with TestClient(app) as client:
response = client.get("/ready")
assert response.json() == {"pool_open": True}
After the with block exits, the lifespan shutdown code runs. If you kept a reference to the pool, you can assert that pool.is_open is False.
To run the application with uvicorn, execute this in your terminal:
uvicorn main:app --host 0.0.0.0 --port 8000
Startup logs appear before the first request. Send SIGINT (Ctrl+C) or SIGTERM; shutdown logs appear after termination. If you raise an exception inside the post-yield finally block, it does not alter an already returned response, but it should appear in server logs.
Limits and common mistakes
- Request-scoped cleanup does not belong in lifespan. Do not store sessions, user data, or request IDs on
app.state. Use dependencies withyieldfor per-request setup and teardown:
async def get_db():
db = Session()
try:
yield db
finally:
db.close()
- Do not block startup with synchronous I/O. Use async drivers or run blocking setup in a threadpool. A slow pre-yield blocks the server from accepting traffic.
- Do not create a new client per request. Reuse the
app.stateclient across requests. Creating a new client inside each endpoint defeats the purpose of a shared pool. - Keep teardown idempotent. Wrap teardown in
try/finallyand make it safe to call multiple times. Exceptions afteryieldhappen during shutdown and may only appear in logs. - Legacy event handlers. The
startupandshutdownevent decorators are legacy. Preferlifespanfor new code, but confirm your FastAPI and Starlette versions before removing existing handlers. - Process-wide state.
app.stateis shared by all requests in the process. With multiple workers, each process has its own state. Guard mutable resources if you rely on cross-request consistency. - TestClient and alternative servers vary. Lifespan behavior can differ by version. Test startup and shutdown explicitly rather than assuming a bare client call runs them.
Practical verification
Check the installed versions in your environment before relying on edge-case ordering:
pip show fastapi starlette
Then confirm the lifecycle end to end: start the server, hit /ready, terminate the process, and verify that shutdown code executed. If the resource is a real client or pool, check that connections are released after shutdown.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.