Application Events & Startup Logic
Learn to use the lifespan context manager for application startup and shutdown logic, including resource initialization and cleanup.
🎯 What You'll Learn
- •Understand the lifespan context manager pattern for startup and shutdown
- •Initialize shared resources during application startup
- •Clean up resources gracefully during application shutdown
- •Use module-level state to share data between lifespan and endpoints
Application Events & Startup Logic
🎯 What You'll Learn
- How to use the lifespan context manager for startup and shutdown logic
- Why the deprecated @app.on_event decorators should be avoided
- Common patterns for initializing and cleaning up resources
- How to share state between lifespan events and request handlers
📚 Theory
Most real-world applications need to perform setup work before they start handling requests -- loading configuration, connecting to databases, initializing caches, or preloading data. Similarly, when the application shuts down, you often need to clean up those resources. FastAPI provides the lifespan context manager for exactly this purpose.
The Lifespan Context Manager
The recommended way to handle startup and shutdown logic in FastAPI is through a lifespan async context manager. This function runs code before the application starts accepting requests (startup) and after it stops (shutdown).
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: runs before the app starts accepting requests
print("Starting up...")
yield
# Shutdown: runs after the app stops accepting requests
print("Shutting down...")
app = FastAPI(lifespan=lifespan)
Everything before yield runs during startup. Everything after yield runs during shutdown. The yield itself is where the application runs and handles requests.
Sharing State Between Lifespan and Endpoints
A common pattern is to use a module-level dictionary or object to share state between the lifespan function and your endpoint handlers. This is because the lifespan runs in the same Python process as your endpoints.
app_state: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load data during startup
app_state["db"] = await connect_to_database()
app_state["cache"] = initialize_cache()
yield
# Clean up during shutdown
await app_state["db"].disconnect()
app_state.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/data/")
async def get_data():
return app_state["cache"].get("data")
Deprecated: @app.on_event
Earlier versions of FastAPI used @app.on_event("startup") and @app.on_event("shutdown") decorators. While these still work, they are deprecated and the lifespan approach is preferred.
# DEPRECATED - do not use in new code
@app.on_event("startup")
async def startup_event():
print("Starting up...")
@app.on_event("shutdown")
async def shutdown_event():
print("Shutting down...")
The lifespan approach is better because:
- It clearly shows the relationship between startup and shutdown logic
- Resources created in startup are naturally available for cleanup in shutdown
- It follows Python's standard context manager pattern
- It is easier to test and reason about
Startup vs Shutdown Responsibilities
Startup is where you:
- Open database connections or connection pools
- Load configuration from files or environment
- Initialize caches and preload frequently accessed data
- Set up background task schedulers
- Connect to external services (message queues, etc.)
Shutdown is where you:
- Close database connections gracefully
- Flush pending writes or logs
- Cancel background tasks
- Release locks or external resources
- Send shutdown signals to connected clients
Error Handling in Lifespan
If an exception occurs during startup (before yield), the application will not start. If an exception occurs during shutdown (after yield), it will be logged but the application will still shut down.
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
app_state["db"] = await connect_to_database()
yield
finally:
# Cleanup runs even if there was an error
if "db" in app_state:
await app_state["db"].disconnect()
🔧 Key Concepts
- Lifespan Context Manager: An async context manager that wraps the entire application lifecycle
- Startup Logic: Code that runs before the app accepts requests (before
yield) - Shutdown Logic: Code that runs after the app stops accepting requests (after
yield) - Module-Level State: A shared dict or object accessible by both lifespan and endpoints
- Resource Cleanup: Ensuring all resources are properly released on shutdown
- Deprecated on_event: The old decorator-based approach that should be avoided in new code
💡 Best Practices
- Always use the lifespan context manager instead of deprecated @app.on_event decorators
- Use try/finally in lifespan to ensure cleanup runs even if startup partially fails
- Keep startup logic fast -- defer heavy loading to background tasks if needed
- Log startup and shutdown events for debugging and monitoring
- Use module-level state carefully and avoid race conditions in concurrent scenarios
- Test lifespan logic independently by calling the context manager directly
🔗 Additional Resources
💡 Hint
Define an async context manager with @asynccontextmanager that yields between startup and shutdown logic. Pass it to FastAPI(lifespan=lifespan). Use a module-level dict to share state between the lifespan and your endpoints.
Ready to Practice?
Now that you understand the theory, let's put it into practice with hands-on coding!
Start Interactive Lesson