FastAPI Lifespan Events
Some work belongs at the very start or the very end of an application's life. A restaurant unlocks the doors and lights the ovens before the first guest arrives. At closing time, staff switch off the ovens and lock up. Lifespan events give FastAPI the same two moments: startup, before the app accepts requests, and shutdown, after the last request finishes.
Typical Startup and Shutdown Tasks
- Open a database connection pool at startup and close it at shutdown.
- Load a large machine learning model into memory once.
- Connect to Redis or another cache service.
- Create background workers and stop them cleanly.
- Flush logs or save pending data before the process exits.
The Timeline
Server starts
│
▼
┌─────────────────────┐
│ STARTUP code runs │ load model, open connections
└──────────┬──────────┘
▼
┌─────────────────────┐
│ App serves requests │ routes handle traffic
└──────────┬──────────┘
▼
Server receives stop signal
▼
┌─────────────────────┐
│ SHUTDOWN code runs │ close connections, save data
└─────────────────────┘Writing a Lifespan Function
A lifespan function uses the asynccontextmanager decorator. Code before yield runs at startup. Code after yield runs at shutdown.
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Starting up")
yield
print("Shutting down")
app = FastAPI(lifespan=lifespan)
Terminal output:
Starting up
INFO: Application startup complete.
...serving requests...
(press CTRL+C)
Shutting downPass the function to FastAPI(lifespan=lifespan). FastAPI calls it automatically.
Sharing Resources Through app.state
Resources created at startup need a place to live. The app.state object stores them for every route to use.
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.model = load_big_model() # runs once
yield
app.state.model = None # cleanup
app = FastAPI(lifespan=lifespan)
@app.get("/predict")
def predict(request: Request):
model = request.app.state.model
return {"result": model.run([1, 2, 3])}Loading a heavy model inside the route would repeat the slow work on every call. Lifespan loads it a single time.
Example with a Connection
import redis.asyncio as redis
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.cache = redis.from_url("redis://localhost:6379")
yield
await app.state.cache.aclose()The connection opens before traffic arrives and closes when the server stops, so no stray connections linger.
The Older on_event Style
Earlier FastAPI versions used @app.on_event("startup") and @app.on_event("shutdown"). Those decorators still work in older projects but carry a deprecated label. Choose the lifespan function for new code. It keeps setup and cleanup together in one place, which makes the logic easier to read.
Testing with Lifespan
The test client runs lifespan code only inside a with block.
from fastapi.testclient import TestClient
with TestClient(app) as client:
response = client.get("/predict")Skipping the with block means startup never runs, and app.state stays empty.
Common Mistakes
Forgetting the yield
A lifespan function without yield raises an error. The keyword marks the boundary between startup and shutdown.
Slow Startup Tasks
Heavy work delays the moment your app accepts traffic. Load only what every request needs.
Skipping Cleanup
Open connections that never close drain resources on the database server. Always release what startup acquired.
A Practical Scenario
An online store loads a product recommendation model that takes eight seconds to read from disk. Without lifespan, the first visitor after every restart waits those eight seconds, and a second visitor may trigger a duplicate load. With lifespan, the server loads the model once before it accepts traffic. Every visitor gets a fast answer from the first request.
Shutdown work matters as much. The same store keeps a small buffer of page-view counts in memory. At shutdown, the lifespan function writes the buffer to the database. No counts vanish when the team deploys a new version.
Keep both phases short and predictable. A startup that takes minutes causes health checks to fail on many hosting platforms, and a shutdown that hangs forces the platform to kill the process.
Key Points
- Lifespan events run code at app startup and shutdown.
- Code before
yieldhandles startup; code after handles shutdown. - Store shared resources on
app.state. - Prefer lifespan over the older
on_eventdecorators. - Use
with TestClient(app)to trigger lifespan in tests.
