Python FastAPI Lifespan: Startup and Shutdown
python fastapi lifespan startup and shutdown: Learn how to manage FastAPI startup and shutdown using the lifespan context manager, including async setup, cleanup, and...
FastAPI applications often need to initialize resources when the server starts and release them when it stops. A database connection pool, an HTTP client, or a cache client are typical examples. The lifespan parameter of the FastAPI app provides a single async context manager that runs startup code before yielding and shutdown code after the yield. This is the recommended way to handle python fastapi lifespan startup and shutdown in modern FastAPI applications.
The Problem with Startup and Shutdown Logic
Before the lifespan parameter became the standard, FastAPI developers used the on_event decorator with "startup" and "shutdown" strings. While that approach works, it has several drawbacks. The event handlers are separate functions, so related setup and teardown logic is split across the codebase. The handlers also lack access to the application's type-safe state, and they are not naturally scoped to a single resource lifecycle. As FastAPI evolved, the lifespan context manager was introduced to address these issues by keeping startup and shutdown code together in one place.
The core idea is simple: you define an async generator that yields once. Code before the yield runs when the application starts, and code after the yield runs when the application shuts down. This pattern matches Python's contextlib conventions and gives you a clean, composable way to manage resources.
The Lifespan Context Manager
To use lifespan, you define an async function that yields an optional dictionary. That dictionary becomes the app.state object, allowing you to store shared resources. Here is a minimal example:
from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app: FastAPI): # Startup: initialize resources app.state.db = await create_db_pool() yield # Shutdown: clean up resources await app.state.db.close() app = FastAPI(lifespan=lifespan)
The yield statement is the dividing line. Everything before it runs during startup, and everything after it runs during shutdown. The app argument is the FastAPI instance, so you can attach resources to app.state and access them later in your routes or dependencies.
If you do not need to store anything in state, you can simply yield with no value. The context manager must still be an async generator, and the yield is required even if you have no startup or shutdown logic, because FastAPI expects the generator to produce a value.
Startup and Shutdown Order and Behavior
FastAPI runs the lifespan context manager exactly once per application lifecycle. When the server starts, it enters the context manager, executes the code before yield, and then waits for requests. When the server shuts down, it resumes the generator after yield and executes the cleanup code. This order is deterministic, which makes it easier to reason about resource initialization and teardown.
If you have multiple resources, you can initialize them sequentially before the yield and clean them up in reverse order after the yield. For example:
@asynccontextmanager async def lifespan(app: FastAPI): # Startup app.state.db = await create_db_pool() app.state.cache = await create_cache_client() yield # Shutdown await app.state.cache.close() await app.state.db.close()
Because the cleanup code runs after the yield, it is executed even if the application encounters an unexpected shutdown signal. This is a key advantage over relying on external process management to release resources.
Error Handling and Exceptions in Lifespan
Exceptions raised during startup prevent the application from starting. If the code before yield raises, FastAPI does not enter the serving state, and the error is propagated to the server runner. This is useful because it surfaces configuration or connection failures immediately rather than letting the application run in a broken state.
Exceptions raised during shutdown are handled differently. If the code after yield raises, FastAPI logs the error but does not prevent the shutdown process from completing. The application still exits, but you should treat cleanup exceptions as serious issues because they often indicate leaked resources. To ensure cleanup runs even when an earlier step fails, use try/finally inside the context manager:
@asynccontextmanager async def lifespan(app: FastAPI): app.state.db = await create_db_pool() try: yield finally: await app.state.db.close()
This pattern guarantees that close() is called even if an exception occurs while the application is serving requests. It is a good practice to wrap resource cleanup in finally blocks to avoid silent resource leaks.
Lifespan vs on_event: What Changed
FastAPI's on_event decorator is now considered legacy. The lifespan parameter is the recommended approach because it offers several concrete improvements:
- Type safety: The
app.stateobject is a regular Python object, so you can attach any attribute without string-based event names. - Single responsibility: Startup and shutdown logic for a resource lives in one function, making it easier to read and maintain.
- Composability: You can use
contextlibutilities likeExitStackto manage multiple resources with a single lifespan. - Testability: The lifespan is a standard async context manager, so you can test it directly without spinning up a server.
If you are maintaining an older FastAPI application that uses @app.on_event("startup") and @app.on_event("shutdown"), migrating to lifespan is straightforward. Move the body of the startup handler before the yield and the body of the shutdown handler after the yield. The lifespan function receives the app instance, so any references to app.state or other app attributes continue to work.
Testing Lifespan with TestClient
FastAPI's TestClient (based on httpx) triggers the lifespan context manager when you use it as a context manager. This means you can test startup and shutdown behavior in your integration tests without launching a real server.
from fastapi.testclient import TestClient with TestClient(app) as client: response = client.get("/") assert response.status_code == 200
When you enter the with block, the startup code runs. When you exit, the shutdown code runs. This is important for tests that rely on resources like a database connection or a temporary directory. If you create the TestClient without the with statement, the lifespan is not executed, so any startup-dependent state will be missing.
You can also test the lifespan function directly by calling it and manually stepping through the generator:
async def test_lifespan(): gen = lifespan(app) await gen.__anext__() # startup # assert resources are initialized await gen.aclose() # shutdown
However, using TestClient is usually simpler and more realistic because it exercises the full ASGI lifecycle.
Production Considerations for Resource Cleanup
In production, the lifespan context manager runs inside the ASGI server (e.g., Uvicorn). The startup code executes before the server begins accepting connections, and the shutdown code executes when the server receives a termination signal. This makes it the right place to acquire and release external resources.
One common pitfall is blocking the event loop during startup or shutdown. Because the lifespan is async, you should avoid synchronous, CPU-bound operations that could block the loop. If you must perform a blocking operation, use asyncio.to_thread or run it in an executor. For example, a synchronous database driver that does not expose an async API should be initialized with await asyncio.to_thread(init_sync_db).
Another consideration is timeout handling. Shutdown code may hang if a resource does not close promptly. Most ASGI servers have a forced shutdown timeout, but you should still design your cleanup to be quick. If you need to wait for in-flight requests to complete, you can use a graceful shutdown pattern that tracks active requests and waits for them to finish before closing resources. The lifespan context manager is the natural place to implement this, because it runs after the server stops accepting new connections but before the process exits.
Finally, remember that the lifespan context manager is a single generator. If you need to share resources across multiple applications or workers, you should manage them at the process level rather than inside the lifespan. The lifespan is per-application, so each worker process runs its own startup and shutdown sequence. This is usually the desired behavior, but it means you cannot rely on a single shared resource pool across workers without an external coordination mechanism like a database or a message broker.