Managing App-Lifetime Resources in FastAPI with Lifespan Context Managers
Use FastAPI lifespan context managers to initialize app-scoped resources once at startup and guarantee cleanup on shutdown, exposing them via dependency injection without globals.
30 Nov 2025, 07:08 UTC

Production FastAPI services need long-lived resources like database pools, cache clients, and background workers to be created once when the process starts and cleaned up reliably on shutdown. Using module globals or mixing legacy startup events often leads to duplicate initialization, missed cleanup on signals, and hidden coupling between requests.
The practical engineering decision is to treat app-scoped resources as a lifespan context manager and expose them to handlers via dependency injection. This gives deterministic startup and shutdown without global state.
Why startup events are fragile
Older FastAPI code used @app.on_event("startup") and @app.on_event("shutdown"). Those handlers run at process level but are decoupled from the ASGI lifespan protocol, making ordering and cancellation harder to reason about. Mixing them with a new lifespan can cause duplicate initialization or cleanup being skipped when the server is stopped with a signal.
Lifespan replaces those events in recent FastAPI and Starlette releases. It is a single async context manager passed to the application. Code before yield runs on startup, code after yield runs on graceful shutdown.
How lifespan enables app-scoped singletons
Because lifespan runs once per process, resources created inside it can be stored in a container and provided to request handlers through dependencies. That avoids globals while keeping the resource alive for the whole app lifetime.
The pattern is:
- Create the resource in the context manager entry.
- Yield control so the app can serve requests.
- Close the resource after yield for deterministic release.
Resources must be app-scoped. Request-scoped work such as per-request DB sessions must not be created here, otherwise they will be shared across requests and leak.
Worked example: pool via lifespan and dependency
The following shows the structure for an app-scoped resource. Run it from the project root with a Python environment that has FastAPI and Uvicorn installed.
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
class FakePool:
def __init__(self):
self.open = True
async def close(self):
self.open = False
pool_container = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
pool = FakePool()
pool_container["pool"] = pool
# initialization logic here
yield
# cleanup logic here
await pool.close()
pool_container.pop("pool", None)
app = FastAPI(lifespan=lifespan)
def get_pool():
return pool_container["pool"]
@app.get("/health")
def health(pool = Depends(get_pool)):
return {"pool_open": pool.open}
Start the server in a terminal:
uvicorn main:app --reload
Required permissions are read access to the project and ability to bind the chosen port. Meaningful placeholders are the module name main and the app variable app. Expected checks are that the application object has a lifespan attribute set and that a request to /health resolves the dependency from the container. A practical verification is to inspect app.lifespan after import and to trigger a graceful shutdown with a termination signal to confirm cleanup code after yield executes.
Trade-offs and limitations
Lifespan is process-level, not per worker. In multi-worker deployments each worker runs its own lifespan, so connection limits must be sized per worker. It is version sensitive; older codebases using only on_event will need migration.
Do not mix lifespan with legacy startup/shutdown events for the same resource. That can cause double initialization and missed cleanup. Also avoid placing request-scoped state inside lifespan; it will be shared and cause leaks and incorrect sharing.
Rollback is relevant because the context manager changes state. If initialization fails before yield, the app should not start serving. Ensure initialization errors are raised so the server fails fast rather than running with a half-initialized resource.
Use lifespan for app-scoped, long-lived resources and keep request-scoped dependencies separate. That gives clear ownership of startup and shutdown with deterministic cleanup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.