Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›FastAPI›Lifespan Events — startup and shutdown

Routing and Middleware

Lifespan Events — startup and shutdown

Lifespan events in FastAPI allow you to define logic that executes precisely when your application starts and shuts down. This mechanism is critical for managing shared resources like database pools, cache connections, or background task runners that must persist throughout the application's lifecycle. By leveraging these events, you ensure your application is properly initialized before receiving requests and cleanly exits to prevent resource leaks.

Understanding the Lifespan Context

The lifespan of a FastAPI application refers to the period between the server process starting and ending. When you define a lifespan event, you are essentially providing a set of instructions that the server must execute around the main event loop. FastAPI uses the concept of a context manager to handle this. The portion of the function before the 'yield' statement acts as the startup sequence, ensuring all dependencies are ready before the server accepts traffic. The code following the 'yield' acts as the shutdown sequence, which runs only after the server has stopped accepting new requests. This separation is vital because it enforces a strict ordering of operations, guaranteeing that your application does not attempt to access a database before the connection pool is fully established, nor does it crash while closing connections while requests are still in flight.

from contextlib import asynccontextmanager
from fastapi import FastAPI

# The lifespan function manages setup and teardown logic
@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: Initialize resources like DB pools here
    print("Initializing application resources...")
    yield
    # Shutdown: Clean up resources here
    print("Cleaning up resources before exit...")

app = FastAPI(lifespan=lifespan)

Managing Database Connections

One of the most common requirements in professional web development is managing persistent database connections efficiently. Creating a new connection for every single incoming request is computationally expensive and quickly exhausts database capacity. Instead, we use the lifespan event to initialize a connection pool at startup and store it within the application state object. By attaching the pool to 'app.state', we make it accessible to every request handler throughout the application's entire runtime. This approach optimizes performance because the pool is pre-warmed and ready to serve queries immediately upon request. During the shutdown phase, we must explicitly call the close method on the database engine. Failing to do so can lead to 'zombie' connections remaining open on the database server, which eventually prevents new connections from being made and leads to stability issues in your production infrastructure.

from contextlib import asynccontextmanager
from fastapi import FastAPI

# Simulating a database engine connection pool
app = FastAPI()

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Setup: Create the engine/pool
    app.state.db_pool = "Connected to Postgres"
    yield
    # Teardown: Close the connection pool
    app.state.db_pool = None
    print("Database pool closed")

Shared Application State

The FastAPI 'app.state' object is a specialized namespace designed to hold shared resources that need to persist across the application lifecycle. Because lifespan events run within the main application context, they are the ideal place to populate this object. When you place a shared variable like a configuration object or a heavy-duty ML model instance into 'app.state', you ensure that the same instance is reused across all threads and workers. This not only conserves memory by avoiding redundant allocations but also ensures consistency. Furthermore, using 'app.state' provides a clean, predictable way to access these globals in your route handlers without resorting to unsafe global variables in your module scope. This pattern encapsulates your dependencies, making the application code easier to test and modify, as all major infrastructure components are clearly defined and instantiated within the lifespan context manager at the entry point of your server.

from contextlib import asynccontextmanager
from fastapi import FastAPI

app = FastAPI()

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Store metadata that routes might need
    app.state.version = "1.0.0"
    yield
    # Clean up state if necessary
    del app.state.version

@app.get("/")
async def get_version():
    return {"version": app.state.version}

Handling Shutdown Cleanup

The shutdown phase of the lifespan event is arguably as important as the startup phase, particularly in cloud environments where instances are frequently cycled or scaled. When a server receives a termination signal, it should stop accepting new requests, allow existing requests to finish processing, and then finalize pending background tasks. Without a properly implemented shutdown event, your application might abruptly kill ongoing database transactions or stop a background write job halfway through, leading to data corruption or inconsistency. By placing cleanup code after the 'yield' keyword, you inform FastAPI to wait until the application is shutting down to execute these instructions. This ensures that resources like file handles, socket connections, and background workers are released gracefully. This graceful termination is a core requirement for high-availability systems, preventing errors and ensuring that the data integrity of your application is preserved during every deployment or restart.

import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup logic
    yield
    # Shutdown: Wait for background tasks to finish gracefully
    print("Shutting down...")
    await asyncio.sleep(1) 
    print("All tasks completed safely")

app = FastAPI(lifespan=lifespan)

Testing Lifespan Integrations

Testing code that relies on lifespan events can be challenging because you need to ensure the startup logic runs before your tests start and the shutdown logic runs after they finish. FastAPI provides a 'TestClient' that is fully compatible with lifespan events, automatically executing them when you use the 'with' statement. This is crucial for integration testing because it allows you to test your routes in a state that exactly mirrors production, including active database pools and initialized state. If your tests fail because a database connection was missing, you know your lifespan logic is incorrectly configured. By wrapping your test client in a context manager, you verify that your application initialization logic is robust and that your teardown procedures do not raise errors when the server stops. This practice prevents 'flaky' tests where local execution differs from the actual production execution environment, ensuring your application behaves reliably across all environments.

from fastapi.testclient import TestClient
from fastapi import FastAPI

app = FastAPI()

# TestClient handles lifespan automatically when used as a context manager
with TestClient(app) as client:
    # The startup event ran before this block
    response = client.get("/")
    assert response.status_code == 404
    # The shutdown event will run after exiting this block

Key points

  • Lifespan events are defined using an asynchronous context manager to handle startup and shutdown logic.
  • The code located before the yield statement in the lifespan function runs during the application startup sequence.
  • The code located after the yield statement in the lifespan function runs during the application shutdown sequence.
  • The app.state object is the recommended location for storing shared resources like database pools throughout the application.
  • Graceful shutdown ensures that ongoing requests and background processes finish execution before the application terminates.
  • Improper cleanup during shutdown can result in database connection leaks and potential data inconsistency issues.
  • The FastAPI TestClient automatically handles lifespan events, making it ideal for robust integration testing of your application.
  • Using lifespan events encapsulates your resource management logic, keeping your route handlers clean and focused on business logic.

Common mistakes

  • Mistake: Performing heavy I/O operations directly in the lifespan decorator function. Why it's wrong: It blocks the application startup process, potentially leading to timeouts or delayed health checks. Fix: Use asyncio.create_task() for background tasks or ensure the setup is non-blocking.
  • Mistake: Forgetting to yield control in the lifespan context manager. Why it's wrong: Without the yield, the application will hang during startup and never actually start serving requests. Fix: Always include the 'yield' statement to indicate where the application runs.
  • Mistake: Trying to access dependency injection components inside the lifespan function. Why it's wrong: Lifespan occurs outside the context of request-response dependency injection. Fix: Manage shared resources like database pools as top-level objects or within the lifespan state.
  • Mistake: Failing to close database connections in the 'finally' block of the lifespan function. Why it's wrong: It causes resource leaks and 'zombie' connections in the database. Fix: Wrap cleanup logic in a 'try...finally' block to guarantee resource release.
  • Mistake: Using global variables instead of the lifespan 'state' dictionary to share resources. Why it's wrong: It makes testing harder and breaks the encapsulation provided by FastAPI. Fix: Attach shared resources to 'app.state' within the lifespan context manager.

Interview questions

What is the purpose of lifespan events in FastAPI?

Lifespan events in FastAPI allow you to execute specific code during the application startup and shutdown phases. This is essential for managing resources that must exist for the duration of the app, such as initializing database connection pools, starting background tasks, or warming up machine learning models. By handling these during startup, you ensure your application is ready to serve requests, and during shutdown, you ensure clean resource disposal, preventing memory leaks or database connection errors.

How do you define a lifespan function using the lifespan context manager?

To define a lifespan function, you create an asynchronous generator that uses the 'yield' keyword. Everything before the 'yield' executes when the application starts, and everything after the 'yield' executes when the application shuts down. You pass this function to the FastAPI constructor using the 'lifespan' parameter. For example: 'async def lifespan(app: FastAPI): startup_code(); yield; shutdown_code()'. This pattern is the modern, recommended way to handle application lifecycles compared to legacy decorators.

Why is it important to use a lifespan context manager instead of standard startup/shutdown event handlers?

The modern lifespan context manager is superior because it ensures that startup and shutdown logic are logically coupled within a single asynchronous scope. Older event handlers, like '@app.on_event("startup")', are deprecated because they can lead to race conditions or inconsistent state management. The lifespan approach allows you to share state between the startup and shutdown phases using the request state object, providing a cleaner, more robust way to manage shared dependencies throughout the application's life.

How can you share data created during startup with your path operation functions?

You can share data by injecting it into the 'app.state' object inside your lifespan generator. Because 'app.state' is accessible within your route handlers, you can store database connections, cache instances, or configuration objects there during startup. For instance: 'app.state.db = await connect_db()'. In your route, you can access it via 'request.app.state.db'. This method is highly efficient as it avoids creating new connections for every individual incoming HTTP request.

Compare the 'lifespan' parameter approach with the legacy 'on_event' approach in FastAPI. Why should we avoid 'on_event'?

The 'lifespan' parameter approach provides a unified, structured lifecycle management system that executes code in a single context block, which guarantees that cleanup always occurs after startup finishes. In contrast, 'on_event' uses disparate, disconnected decorators that are difficult to coordinate, making it harder to track resource dependencies. 'on_event' is officially deprecated because it cannot guarantee the same ordering or error handling as the lifespan context manager, often leading to unclosed resources when the application terminates unexpectedly.

What happens if an exception occurs during the startup phase of the lifespan function, and how does it affect the application?

If an exception occurs during the startup section of your lifespan function—the part before the 'yield'—FastAPI will treat it as a critical failure. The application will not complete its initialization and will fail to start. This is actually a desired behavior in production, known as 'fail-fast.' It prevents the application from entering an unstable state where it might try to process incoming traffic without necessary resources like a database or a required configuration file, protecting the system from inconsistent data.

All FastAPI interview questions →

Check yourself

1. When you use a lifespan context manager, what happens to the application after the 'yield' statement?

  • A.The application immediately shuts down.
  • B.The application begins accepting and processing requests.
  • C.The application enters a suspended state waiting for an external signal.
  • D.The application triggers a secondary startup sequence.
Show answer

B. The application begins accepting and processing requests.
The 'yield' acts as the marker where the app is ready; code before it runs at startup, and code after it runs at shutdown. Option 0 is wrong because the app stays running. Option 2 and 3 are incorrect interpretations of the flow control.

2. Why is it recommended to use 'app.state' within a lifespan event instead of global variables?

  • A.It increases the speed of request processing.
  • B.It allows for typed configuration of the application.
  • C.It provides a clear, managed scope for sharing resources across the application lifecycle.
  • D.It automatically garbage collects resources when a request finishes.
Show answer

C. It provides a clear, managed scope for sharing resources across the application lifecycle.
app.state is the idiomatic FastAPI way to store shared objects like database pools. Option 0 is false as state access speed is negligible. Option 1 is not the primary purpose. Option 3 is wrong because state persists for the app lifetime, not per request.

3. What is the primary purpose of the 'finally' block in a lifespan context manager?

  • A.To restart the application if a database connection fails.
  • B.To ensure cleanup tasks like closing connections run even if an error occurs during startup.
  • C.To log the duration of the application's uptime.
  • D.To force the application to accept requests before cleanup completes.
Show answer

B. To ensure cleanup tasks like closing connections run even if an error occurs during startup.
The 'finally' block ensures cleanup happens regardless of whether the app started successfully or crashed. Option 0 is a misinterpretation of lifecycle. Option 2 is not the purpose of 'finally'. Option 3 is incorrect because the app shouldn't accept traffic during teardown.

4. If you need to run a periodic background task that lives for the duration of the app, where should you initialize it?

  • A.Inside a dependency injected into every endpoint.
  • B.Inside the lifespan function, using asyncio.create_task after the yield.
  • C.Inside the lifespan function, before the yield.
  • D.Directly in the main module scope outside of any function.
Show answer

C. Inside the lifespan function, before the yield.
Initializing it before the yield ensures it starts before the app receives requests. Option 0 is inefficient. Option 1 runs after the app is already serving requests. Option 3 is global scope, which is bad practice.

5. What happens if a lifespan event function raises an unhandled exception during the startup phase?

  • A.The application starts anyway but logs a warning.
  • B.The application starts but disables all endpoints.
  • C.The application fails to start and shuts down.
  • D.The application enters a recovery mode automatically.
Show answer

C. The application fails to start and shuts down.
FastAPI treats startup exceptions as critical failures; if the app cannot set up its resources, it should not start. Options 0, 1, and 3 suggest the app survives in a broken state, which is not the default behavior.

Take the full FastAPI quiz →

← PreviousBackground TasksNext →OAuth2 Password Flow with JWT

FastAPI

24 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app