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 down

Pass 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 yield handles startup; code after handles shutdown.
  • Store shared resources on app.state.
  • Prefer lifespan over the older on_event decorators.
  • Use with TestClient(app) to trigger lifespan in tests.