Skip to main content
FastAPI’s dependency injection system supports advanced patterns that enable powerful architectural designs. This guide explores async dependencies, caching strategies, generator-based dependencies with cleanup, and dependency scopes.

Async Dependencies

FastAPI fully supports asynchronous dependencies, which is essential for I/O-bound operations like database queries or API calls.

Basic Async Dependency

from fastapi import Depends, FastAPI
import httpx

app = FastAPI()

async def get_http_client():
    async with httpx.AsyncClient() as client:
        yield client

@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    client: httpx.AsyncClient = Depends(get_http_client)
):
    response = await client.get(f"https://api.example.com/users/{user_id}")
    return response.json()
Async dependencies are only resolved when used in async path operations. In sync path operations, they’re executed in a threadpool.

Mixing Async and Sync Dependencies

You can mix async and sync dependencies freely:
def get_database() -> Database:
    return Database()

async def get_current_user(
    db: Database = Depends(get_database),
    token: str = Header(...)
) -> User:
    # Async operation using sync dependency
    user = await db.get_user_by_token(token)
    return user

@app.get("/profile")
async def read_profile(user: User = Depends(get_current_user)):
    return user
Be careful when calling sync dependencies from async code. If a sync dependency performs blocking I/O, it can block the event loop. FastAPI runs sync dependencies in a threadpool to mitigate this.

Dependency Caching

By default, FastAPI caches dependency results within a single request. This prevents expensive operations from running multiple times.

How Caching Works

from fastapi import Depends, FastAPI

app = FastAPI()

call_count = {"count": 0}

def expensive_dependency():
    call_count["count"] += 1
    # Simulate expensive operation
    return {"result": "data", "calls": call_count["count"]}

def depends_on_expensive(data: dict = Depends(expensive_dependency)):
    return data

@app.get("/cached")
def read_cached(
    # Both parameters use the same dependency
    data1: dict = Depends(expensive_dependency),
    data2: dict = Depends(expensive_dependency),
    data3: dict = Depends(depends_on_expensive)
):
    # expensive_dependency is only called ONCE per request
    assert data1 is data2 is data3
    return data1  # Will show calls: 1

Cache Keys

Dependencies are cached based on:
  • The dependency callable itself
  • OAuth2 scopes (if using Security)
  • The dependency scope (function or request)
See fastapi/dependencies/models.py:63-71 for the cache key implementation.

Disabling Cache

Disable caching for dependencies that must run every time:
from fastapi import Depends

def get_timestamp():
    import time
    return time.time()

@app.get("/no-cache")
def read_no_cache(
    time1: float = Depends(get_timestamp, use_cache=False),
    time2: float = Depends(get_timestamp, use_cache=False)
):
    # get_timestamp is called twice, different values
    assert time1 != time2
    return {"time1": time1, "time2": time2}
Use use_cache=False for:
  • Random number generators
  • Timestamp functions
  • Any dependency with side effects that must execute every time

Generator Dependencies with Cleanup

Generator dependencies (using yield) enable setup and teardown logic, perfect for managing resources.

Basic Generator Dependency

from typing import Annotated
from fastapi import Depends, FastAPI

app = FastAPI()

def get_db():
    db = Database()
    try:
        yield db
    finally:
        db.close()

@app.get("/users")
def read_users(db: Annotated[Database, Depends(get_db)]):
    return db.query("SELECT * FROM users")
The code after yield executes after the response is sent, ensuring proper cleanup even if exceptions occur.

Async Generator Dependencies

async def get_async_db():
    db = await AsyncDatabase.connect()
    try:
        yield db
    finally:
        await db.disconnect()

@app.get("/async-users")
async def read_async_users(
    db: Annotated[AsyncDatabase, Depends(get_async_db)]
):
    return await db.fetch_all("SELECT * FROM users")

Exception Handling in Dependencies

from fastapi import HTTPException

def get_db_with_transaction():
    db = Database()
    db.begin_transaction()
    try:
        yield db
        db.commit()
    except HTTPException:
        # HTTPExceptions are caught and re-raised
        db.rollback()
        raise
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()
Code after yield in dependencies runs after the response is sent. You cannot modify the response in the cleanup code.

Dependency Scopes

Dependencies can have different scopes that control their lifecycle:

Function Scope

Function-scoped dependencies are created and destroyed with each path operation function:
from typing import Annotated
from fastapi import Depends

def get_function_resource():
    resource = Resource()
    yield resource
    resource.cleanup()

FunctionDep = Annotated[
    Resource,
    Depends(get_function_resource, scope="function")
]

@app.get("/stream")
def stream_data(resource: FunctionDep):
    def generate():
        # Resource is available during streaming
        for chunk in resource.get_chunks():
            yield chunk
        # Resource cleanup happens after streaming completes
    return StreamingResponse(generate())
scope="function" is required when using generator dependencies with streaming responses. The dependency remains active until the generator completes.

Request Scope

Request-scoped dependencies (the default for generators) are tied to the request lifecycle:
def get_request_resource():
    resource = Resource()
    yield resource
    resource.cleanup()

RequestDep = Annotated[
    Resource,
    Depends(get_request_resource, scope="request")
]
Request-scoped dependencies cannot depend on function-scoped dependencies. This would violate the lifecycle hierarchy.

Scope Rules

  1. Regular (non-generator) dependencies have no scope
  2. Generator dependencies default to scope="request"
  3. Function-scoped dependencies can only depend on other function-scoped dependencies
  4. Request-scoped dependencies cannot depend on function-scoped dependencies
See fastapi/dependencies/utils.py:315-326 for scope validation.

Advanced Patterns

Class-Based Dependencies

Create reusable dependency classes:
from typing import Annotated

class DatabaseSession:
    def __init__(self, db_url: str = "postgresql://localhost/db"):
        self.db_url = db_url
        self.connection = None

    def __call__(self):
        self.connection = connect(self.db_url)
        try:
            yield self.connection
        finally:
            self.connection.close()

# Create instance
get_db = DatabaseSession(db_url="postgresql://prod/db")

@app.get("/users")
def read_users(db: Annotated[Connection, Depends(get_db)]):
    return db.execute("SELECT * FROM users")

Parameterized Dependencies

Create dependency factories:
from functools import partial

def get_resource_by_type(resource_type: str):
    def _get_resource():
        return ResourceFactory.create(resource_type)
    return _get_resource

# Create specific dependencies
get_cache = get_resource_by_type("cache")
get_queue = get_resource_by_type("queue")

@app.get("/data")
def read_data(
    cache = Depends(get_cache),
    queue = Depends(get_queue)
):
    return {"cache": cache, "queue": queue}

Nested Dependencies with Context

from contextvars import ContextVar

request_id_var: ContextVar[str] = ContextVar("request_id")

def get_request_id(request_id: str = Header(...)):
    request_id_var.set(request_id)
    return request_id

def get_logger(request_id: str = Depends(get_request_id)):
    return Logger(request_id=request_id)

@app.get("/process")
def process_request(logger: Logger = Depends(get_logger)):
    logger.info("Processing request")
    # request_id is available in context
    return {"request_id": request_id_var.get()}

Best Practices

Use async for I/O-bound operations: Database queries, API calls, and file operations benefit from async dependencies.
Keep dependencies focused: Each dependency should have a single responsibility. Compose complex dependencies from simpler ones.
Cache expensive operations: Let FastAPI’s caching work for you. Only disable when necessary.
Generator cleanup is guaranteed: Code after yield always executes, even if the path operation raises an exception.
Don’t modify responses in cleanup: The response is already sent before cleanup code runs.

See Also

Build docs developers (and LLMs) love