Advanced FastAPI Patterns • Lesson 2

Async Dependencies & Lifespan

Learn how to use async dependencies and the lifespan context manager to manage application startup and shutdown resources.

🎯 What You'll Learn

  • Use the lifespan context manager for app startup and shutdown logic
  • Create async dependencies that provide shared resources
  • Understand resource management with lifespan vs deprecated on_event
  • Build endpoints that consume async dependencies

Async Dependencies & Lifespan

What You'll Learn

In this lesson you will learn how to manage application lifecycle events and create async dependencies in FastAPI. You will understand:

  • How the lifespan context manager replaces the deprecated on_event decorators
  • How to initialize and clean up shared resources at startup and shutdown
  • How to create async dependency functions
  • How to wire everything together with Depends

Theory

Application Lifecycle

Every web application needs to perform setup when it starts (connect to databases, load configuration, warm up caches) and cleanup when it stops (close connections, flush buffers). FastAPI provides the lifespan context manager for this purpose.

The Old Way: on_event (Deprecated)

Previously, FastAPI used @app.on_event("startup") and @app.on_event("shutdown") decorators:

# DEPRECATED - do not use in new code
@app.on_event("startup")
async def startup():
    app.state.db = connect_to_database()

@app.on_event("shutdown")
async def shutdown():
    app.state.db.close()

This approach has been deprecated because it separates logically related code (setup and teardown of the same resource) into two different functions.

The New Way: Lifespan Context Manager

The lifespan context manager keeps startup and shutdown logic together:

from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: runs before the app starts accepting requests
    app.state.db = {"items": []}
    print("Database initialized")
    yield
    # Shutdown: runs after the app stops accepting requests
    app.state.db.clear()
    print("Database cleaned up")

app = FastAPI(lifespan=lifespan)

Everything before yield runs at startup. Everything after yield runs at shutdown. The yield itself is where the application runs and handles requests.

Storing State with app.state

FastAPI (via Starlette) provides app.state as a place to store application-wide data:

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.db = {"users": [], "items": []}
    app.state.config = {"debug": True}
    yield

Any data attached to app.state is accessible from any request via request.app.state.

Async Dependencies

Dependencies in FastAPI can be async functions. This is useful when the dependency needs to perform async I/O:

from fastapi import Depends, Request

async def get_db(request: Request):
    return request.app.state.db

@app.get("/items/")
async def list_items(db: dict = Depends(get_db)):
    return {"items": db["items"]}

The dependency function get_db is called for every request. It reads the shared database from app.state and provides it to the endpoint.

Dependencies with Yield (Async)

You can also create async dependencies that perform cleanup after the request:

async def get_db_session(request: Request):
    session = request.app.state.db.create_session()
    try:
        yield session
    finally:
        await session.close()

This pattern is especially useful for database sessions or other resources that need to be released after each request.

Key Concepts

Lifespan Flow

  1. Application starts
  2. Lifespan code before yield executes (startup)
  3. Application accepts and handles requests
  4. Application receives shutdown signal
  5. Lifespan code after yield executes (shutdown)

Dependency Injection Chain

Dependencies can depend on other dependencies, forming a chain:

async def get_db(request: Request):
    return request.app.state.db

async def get_items_service(db: dict = Depends(get_db)):
    return db["items"]

@app.get("/items/")
async def list_items(items: list = Depends(get_items_service)):
    return {"items": items, "count": len(items)}

Best Practices

  1. Always use lifespan instead of on_event decorators in new code
  2. Store shared resources in app.state rather than global variables when possible
  3. Keep lifespan logic minimal -- just initialization and cleanup
  4. Use async dependencies when accessing async resources; use sync dependencies for simple logic
  5. Handle errors in lifespan -- wrap startup code in try/except to fail gracefully
  6. Use yield dependencies for resources that need per-request cleanup (like DB sessions)

Additional Resources

💡 Hint

Use '@asynccontextmanager async def lifespan(app)' to initialize resources at startup. Store them in app.state. Create an async dependency function that reads from app.state and yields the resource.

Ready to Practice?

Now that you understand the theory, let's put it into practice with hands-on coding!

Start Interactive Lesson