Difficulty: Intermediate
Understanding the full request/response lifecycle in FastAPI helps you design better applications and debug issues effectively. The lifecycle spans from application startup to individual request handling to graceful shutdown, with middleware, dependencies, and handlers each playing a role.
The application lifecycle begins with startup events. In modern FastAPI, this is handled using the lifespan context manager. Code before the yield runs during startup (initializing database connections, loading ML models, warming caches), and code after the yield runs during shutdown (closing connections, cleaning up resources). This replaces the older @app.on_event("startup") and @app.on_event("shutdown") decorators.
For each individual request, the lifecycle is: ASGI server receives the connection, middleware processes the request (from outermost to innermost), FastAPI matches the route, dependencies are resolved (including sub-dependencies), the handler executes, the response passes back through middleware (from innermost to outermost), yield dependencies' cleanup code runs, and the ASGI server sends the response.
Dependency resolution order is deterministic. FastAPI builds a dependency graph, resolves leaf dependencies first, and works up to the root. Dependencies at the same level are resolved in the order they appear in the function signature. Yield dependencies are special - their cleanup code runs after the response is sent back through middleware.
Background tasks add another dimension to the lifecycle. Using BackgroundTasks, you can schedule work to happen after the response is sent to the client. The response is returned immediately, and the background task runs afterward. This is useful for sending emails, processing data, or any work that the client does not need to wait for.
Error handling intersects with the lifecycle at multiple points. Middleware can catch and transform errors. Dependencies that raise HTTPException prevent the handler from running. Unhandled exceptions in handlers are caught by FastAPI and converted to 500 responses. Exception handlers registered with @app.exception_handler() provide custom error formatting.
from contextlib import asynccontextmanager
from fastapi import FastAPI
# Simulated resources
db_connection = {}
ml_model = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: runs before accepting requests
print("Starting up...")
db_connection["pool"] = "Connected to database"
ml_model["classifier"] = "Model loaded"
print("Ready to accept requests")
yield # Application runs here
# Shutdown: runs after server stops accepting requests
print("Shutting down...")
db_connection.clear()
ml_model.clear()
print("Cleanup complete")
app = FastAPI(lifespan=lifespan)
@app.get("/")
def root():
return {
"db": db_connection.get("pool"),
"model": ml_model.get("classifier"),
}
The lifespan context manager handles startup (before yield) and shutdown (after yield). Resources initialized during startup are available to all handlers. This replaces the deprecated on_event decorators.
from fastapi import FastAPI, Request, Depends
from starlette.middleware.base import BaseHTTPMiddleware
import time
app = FastAPI()
# Step 1: Middleware (runs first and last)
class LifecycleMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
print("1. Middleware: before request")
request.state.start_time = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - request.state.start_time
print(f"5. Middleware: after response ({duration:.4f}s)")
return response
app.add_middleware(LifecycleMiddleware)
# Step 2: Dependency (runs after middleware, before handler)
def get_db():
print("2. Dependency: creating session")
db = {"session": "open"}
try:
yield db
finally:
print("4. Dependency: closing session")
# Step 3: Handler (runs in the middle)
@app.get("/")
def root(db: dict = Depends(get_db)):
print("3. Handler: processing request")
return {"status": "ok"}
# Output order for GET /:
# 1. Middleware: before request
# 2. Dependency: creating session
# 3. Handler: processing request
# 4. Dependency: closing session
# 5. Middleware: after response (0.0012s)
The lifecycle proceeds in order: middleware before -> dependencies -> handler -> dependency cleanup -> middleware after. Understanding this order helps you know where to place different types of logic.
from fastapi import FastAPI, BackgroundTasks
import time
app = FastAPI()
def send_email(to: str, subject: str):
# Simulated slow operation
time.sleep(2)
print(f"Email sent to {to}: {subject}")
def write_log(message: str):
with open("app.log", "a") as f:
f.write(f"{message}\n")
@app.post("/register")
def register(
email: str,
background_tasks: BackgroundTasks,
):
# Response is sent immediately
# Background tasks run after the response
background_tasks.add_task(send_email, email, "Welcome!")
background_tasks.add_task(write_log, f"New user: {email}")
return {"message": "Registration successful"}
# Client receives the response immediately
# Email is sent and log is written in the background
# Background tasks run sequentially after the response
BackgroundTasks allows scheduling work after the response is sent. The client does not wait for background tasks to complete. Multiple tasks run sequentially. This is lighter than a full task queue (like Celery) for simple cases.
Lifecycle, Startup, Shutdown, Lifespan, Event Handlers