Skip to main content

Router Lifespan and Event Handlers

FastAPI provides a robust system for managing the lifecycle of your application and its individual components. While you often define startup and shutdown logic at the application level, the APIRouter class allows you to encapsulate this logic within specific modules, ensuring that resources like database pools or external clients are only initialized when the relevant parts of your API are active.

The Lifespan Context Manager

The modern way to handle startup and shutdown in FastAPI is through the lifespan parameter. This parameter accepts an async context manager that defines what should happen before the application starts receiving requests and after it finishes handling them.

When you define a lifespan for an APIRouter, you can manage resources specific to that router's domain.

from contextlib import asynccontextmanager
from fastapi import APIRouter, FastAPI

@asynccontextmanager
async def lifespan(router: APIRouter):
# Startup: Initialize a resource
print("Initializing router-specific resource")
yield {"resource_active": True}
# Shutdown: Clean up the resource
print("Cleaning up router-specific resource")

router = APIRouter(lifespan=lifespan)

@router.get("/items/")
async def read_items():
return [{"item": "Portal Gun"}]

app = FastAPI()
app.include_router(router)

How it Works Internally

When you initialize an APIRouter in fastapi/routing.py, the constructor determines how to handle the provided lifespan function. FastAPI is flexible and supports several formats:

  1. Async Generators: If you provide an async def function with a yield, FastAPI automatically wraps it using contextlib.asynccontextmanager.
  2. Synchronous Generators: If you provide a standard def function with a yield, FastAPI wraps it using an internal utility _wrap_gen_lifespan_context to make it compatible with the async lifespan interface.
  3. Default Behavior: If no lifespan is provided, FastAPI uses the internal _DefaultLifespan class. This class is designed to maintain backward compatibility by executing any legacy on_startup and on_shutdown handlers you might have defined.

Nested Lifespans and Merging

One of the most powerful features of FastAPI's router system is the ability to nest lifespans. When you include one router into another (or into the main FastAPI app) using include_router, FastAPI merges their lifespan logic.

In fastapi/routing.py, the include_router method uses _merge_lifespan_context to combine the existing lifespan with the new one:

# From fastapi/routing.py
self.lifespan_context = _merge_lifespan_context(
self.lifespan_context,
router.lifespan_context,
)

State Merging Logic

When lifespans are merged, the "state" (the dictionary yielded by the context managers) is also combined. This allows different parts of your application to contribute to a shared state object accessible in your dependencies and routes.

The merging logic in _merge_lifespan_context follows a specific precedence:

# Internal logic in fastapi/routing.py
yield {**(maybe_nested_state or {}), **(maybe_original_state or {})}

If there is a key collision between the parent (original) state and the child (nested) state, the parent state takes precedence because it is unpacked second in the dictionary literal.

Legacy Event Handlers

Before the lifespan interface was introduced, FastAPI used startup and shutdown events. While these are now deprecated in favor of lifespan, they are still supported for backward compatibility.

You can define these using the on_event decorator or by passing lists to the APIRouter constructor:

router = APIRouter()

@router.on_event("startup")
async def startup_event():
print("Router starting up...")

@router.on_event("shutdown")
def shutdown_event():
print("Router shutting down...")

The _DefaultLifespan Bridge

To ensure these legacy handlers still work within the modern ASGI lifespan requirement, FastAPI uses the _DefaultLifespan class in fastapi/routing.py. This class acts as a bridge:

class _DefaultLifespan:
def __init__(self, router: "APIRouter") -> None:
self._router = router

async def __aenter__(self) -> None:
await self._router._startup()

async def __aexit__(self, *exc_info: object) -> None:
await self._router._shutdown()

When the application starts, _DefaultLifespan.__aenter__ calls the router's internal _startup() method, which iterates through and executes all functions registered in self.on_startup. The same process happens for shutdown.

Path Prefix Requirements

When organizing routers that use lifespans or events, ensure your prefix configuration is correct. FastAPI enforces strict rules in the APIRouter constructor to prevent common routing errors:

  • A path prefix must start with a forward slash (/).
  • A path prefix must NOT end with a forward slash.

If these conditions aren't met, FastAPI raises an AssertionError during initialization to prevent broken URL generation at runtime.