FastAPI applications often depend on resources that are expensive to create for every request. These include HTTP clients, database connection pools, machine learning models, Redis connections, and background service clients.

Creating a new HTTP client for every incoming request can waste connections and increase resource usage. Loading a machine learning model repeatedly can make an endpoint unnecessarily slow. Similarly, failing to close clients and connection pools during application shutdown can leave resources open longer than necessary.

FastAPI provides the lifespan mechanism to manage resources during an application's startup and shutdown. It allows an application to initialize shared resources once, make them available to request handlers, and clean them up when the application stops.

This article explains how FastAPI lifespan works, how to share an asynchronous HTTP client safely, and how to avoid common resource-management mistakes in production applications.

Understanding FastAPI Lifespan

An application has a lifecycle that extends beyond individual requests. During startup, it may need to establish connections or load configuration. While running, it handles requests using those resources. During shutdown, it should release resources properly.

FastAPI supports this lifecycle through an asynchronous context manager passed to the application's FastAPI constructor.

The general structure looks like this:

Python
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: initialize resources.
    print("Application starting")

    yield

    # Shutdown: release resources.
    print("Application stopping")


app = FastAPI(lifespan=lifespan)

The code before yield runs during application startup. The code after yield runs during application shutdown.

The lifespan function is not executed for every request. Instead, it manages resources for the lifetime of the application instance within a running worker.

This distinction is important when designing applications that use shared clients or expensive initialization processes.

Why Shared HTTP Clients Matter

Consider an application that calls an external service whenever a user requests information.

A basic implementation might create an HTTP client inside the endpoint:

Python
import httpx

from fastapi import FastAPI

app = FastAPI()


@app.get("/external-data")
async def get_external_data():
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.example.com/data"
        )

    response.raise_for_status()
    return response.json()

This example demonstrates asynchronous HTTP requests, but it creates and closes an HTTP client for every request.

An HTTP client can maintain a connection pool, reuse connections, and manage common request settings. Creating a new client repeatedly prevents the application from taking full advantage of a long-lived connection pool.

A better design creates the client during application startup and closes it during shutdown.

For example, an application can use httpx.AsyncClient as a shared resource managed by FastAPI lifespan.

Creating a Shared HTTP Client With Lifespan

The following example initializes one asynchronous HTTP client for each application instance.

Python
from contextlib import asynccontextmanager

import httpx
from fastapi import FastAPI, Request


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with httpx.AsyncClient(
        timeout=httpx.Timeout(10.0),
        limits=httpx.Limits(
            max_connections=100,
            max_keepalive_connections=20
        )
    ) as client:
        app.state.http_client = client
        yield


app = FastAPI(lifespan=lifespan)


@app.get("/external-data")
async def get_external_data(request: Request):
    client: httpx.AsyncClient = request.app.state.http_client

    response = await client.get(
        "https://api.example.com/data"
    )
    response.raise_for_status()

    return response.json()

Let's examine the important parts.

Initialize the client before accepting requests

The lifespan function creates the HTTP client before reaching yield. The application can then use that client while it is running.

The timeout configuration limits how long the application waits for an HTTP operation. Connection limits help control how many connections the client maintains.

These values are examples, not universal production defaults. Select limits based on the external service's capacity, the application's expected concurrency, and the HTTP client's behavior.

Store the resource in application state

The line below stores the client on the application instance:

Python
app.state.http_client = client

The endpoint accesses it through:

Python
request.app.state.http_client

This avoids creating a new client for each request.

Application state is appropriate for resources shared by handlers in the same application instance. It should not be confused with process-wide or deployment-wide storage.

Close the client during shutdown

The client is managed by an async with block. When the lifespan context exits, the client is closed automatically.

This releases resources managed by the HTTP client and allows its connections to be cleaned up correctly.

Using a context manager is safer than relying on developers to remember to close the client manually.

Sharing Multiple Resources

Real applications often require more than one shared resource. For example, an application might use an HTTP client for external APIs and a database connection pool for database access.

You can initialize multiple resources in the same lifespan function.

Python
from contextlib import AsyncExitStack, asynccontextmanager

import httpx
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with AsyncExitStack() as stack:
        http_client = await stack.enter_async_context(
            httpx.AsyncClient(timeout=10.0)
        )

        app.state.http_client = http_client

        # Initialize other asynchronous context-managed
        # resources here and register them with stack.

        yield


app = FastAPI(lifespan=lifespan)

AsyncExitStack is useful when an application manages several asynchronous context managers. Each resource registered with the stack is closed when the stack exits.

This approach is especially helpful when initialization involves multiple resources that must be cleaned up if a later initialization step fails.

For a database connection pool, use the initialization and cleanup APIs provided by the specific database library. Do not assume every database driver exposes the same interface.

Using Lifespan With Dependency Injection

Accessing request.app.state directly is simple, but a larger application may benefit from FastAPI dependencies.

Dependencies provide a consistent way to access shared resources and make endpoint code easier to test.

Python
import httpx

from fastapi import Depends, FastAPI, Request


def get_http_client(
    request: Request
) -> httpx.AsyncClient:
    return request.app.state.http_client


@app.get("/products")
async def get_products(
    client: httpx.AsyncClient = Depends(get_http_client)
):
    response = await client.get(
        "https://api.example.com/products"
    )
    response.raise_for_status()

    return response.json()

This example assumes the lifespan function from the previous section has already initialized app.state.http_client.

The dependency retrieves the shared client without creating a new instance. The endpoint focuses on making the request and processing the response.

Dependency injection also makes testing easier because the dependency can be overridden with a test client or mock.

For example, a test can replace the HTTP client dependency with a controlled implementation that returns predictable responses. This avoids depending on an external service during every test run.

Handling Startup Failures Correctly

Resource initialization can fail for several reasons:

  • An external service is unavailable.
  • A required configuration value is missing.
  • A database connection cannot be established.
  • A model file cannot be loaded.
  • A required credential is invalid.

The application should not silently continue when a resource is essential to serving requests.

Consider a resource that must be available before the application can function:

Python
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    resource = await initialize_required_resource()

    app.state.resource = resource

    try:
        yield
    finally:
        await resource.close()


app = FastAPI(lifespan=lifespan)

Here, initialize_required_resource() and resource.close() represent application-specific asynchronous functions. They must be implemented according to the resource library being used.

If initialization fails before yield, the application does not enter the normal serving phase through this lifespan context. That is preferable to accepting requests when a required dependency is unavailable.

The try and finally structure ensures that cleanup runs after successful initialization when the lifespan exits. For multiple resources, AsyncExitStack can simplify cleanup and handle resources registered before a later initialization step fails.

Optional resources require a different policy. If the application can function without a resource, it may be reasonable to log the failure and use a fallback. That decision should be explicit rather than silently ignoring exceptions.

Lifespan and Multiple Application Workers

One common misunderstanding is assuming that a resource created during lifespan is shared by every worker in a deployment.

That is not how application state works.

Suppose a server starts four worker processes. Each process runs its own application instance and initializes its own lifespan resources.

The result may look like this:

Deployment setup

Expected resource instances

One worker with one application instance

One shared client for that instance

Four worker processes

Typically four clients, one per application instance

Two separate application replicas, each with four workers

Typically eight clients in total

Multiple requests handled by one application instance

Requests share that instance's client

These counts assume each worker creates one application instance and one resource. Actual behavior depends on the server configuration and application deployment.

This matters when configuring connection pools. If each worker can maintain up to 100 connections, four workers could potentially maintain up to 400 connections. The external service must be able to handle the combined load.

Lifespan is intended for resources local to an application instance. It is not a mechanism for coordinating shared mutable state across processes.

For shared data or coordination, use an appropriate external system, such as a database, Redis, or a message broker.

Testing FastAPI Lifespan

A test should verify not only endpoint behavior but also resource initialization and cleanup.

When using FastAPI's TestClient, enter it as a context manager to ensure that lifespan startup and shutdown are executed.

Python
from fastapi.testclient import TestClient


def test_application_startup_and_shutdown():
    with TestClient(app) as client:
        response = client.get("/external-data")

        assert response.status_code == 200

This example assumes that the endpoint's external dependency is available and returns a successful response. For a reliable automated test, override the HTTP client dependency or use a controlled test server instead of relying on a live external API.

For asynchronous tests, use an appropriate async testing setup and verify that the lifespan context is actually entered. Simply constructing a test client does not always guarantee that startup and shutdown have been exercised.

Tests should also cover initialization failures, request timeouts, external service errors, and resource cleanup where those behaviors are important to the application.

Common Mistakes to Avoid

Mistake

Why it causes problems

Better approach

Creating an HTTP client inside every endpoint call

Repeated setup prevents effective connection reuse

Create the client during lifespan

Forgetting to close shared resources

Connections and other resources may remain open

Use context managers or AsyncExitStack

Treating application state as global shared storage

Separate workers have separate application instances

Use an external system for cross-worker coordination

Ignoring required resource initialization failures

Requests may reach an application that cannot function correctly

Fail startup when a required resource cannot be initialized

Performing long-running work during every startup

Increases restart and deployment time

Initialize only what the application needs

Assuming tests automatically run lifespan hooks

Startup and cleanup may not be exercised

Use the appropriate test context manager or async lifespan setup

Best Practices for Production Applications

Set explicit timeouts

Every external HTTP request should have an appropriate timeout. Without one, a slow or unresponsive service can consume application resources for longer than intended.

Configure connection limits based on workload and deployment size rather than copying example values without evaluation.

Keep resource ownership clear

The component that creates a shared resource should generally be responsible for closing it. Endpoints should use the resource without closing it after individual requests.

Otherwise, one request could close a client that other requests still need.

Avoid unnecessary global state

Store application-specific resources in app.state or expose them through dependencies. This keeps their lifecycle tied to the application instance and makes testing more manageable.

Monitor resource health

Track relevant metrics such as request latency, timeout rates, connection-pool usage, and external service errors. Reusing a client improves resource management, but it does not remove the need for observability and error handling.

Advantages and Disadvantages

The lifespan mechanism offers several benefits:

  • Resources are initialized once per application instance instead of once per request.
  • Context managers provide a predictable cleanup mechanism.
  • Shared clients can reuse connection pools.
  • Startup and shutdown logic stays separate from endpoint business logic.
  • Dependencies make shared resources easier to replace during testing.

There are also trade-offs. Each worker may create its own resource instances, increasing total connection usage. A failed initialization can prevent the application from starting. In addition, shared clients must be used according to the concurrency guarantees and lifecycle rules of their underlying libraries.

These trade-offs are manageable when resource ownership, configuration, and cleanup are designed explicitly.

Summary

FastAPI lifespan provides a structured way to initialize and release resources that an application needs while it is running.

For an asynchronous HTTP client, create the client during startup, store it on the application instance, access it through request handlers or dependencies, and close it during shutdown. Apply the same lifecycle principles to database pools, Redis clients, and other resources using the appropriate library-specific APIs.

Remember that resources are normally shared within one application instance, not automatically across every worker or replica. Configure connection limits accordingly, handle startup failures deliberately, and test the complete lifecycle.

With these practices, FastAPI applications can reuse resources efficiently without losing control over cleanup, concurrency, or failure handling.