Skip to content
Beyond the Stack

Inside FastAPI · Part 2/2

How FastAPI resolves a dependency

One Depends parameter can hide a tree of sub-dependencies, per-request cache entries, and cleanup code. Follow the solve order and see where request-scoped resources begin and end.

Published 4 min read

This signature looks like it takes three arguments:

@app.get("/me")
async def read_me(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    return user

At request time it runs at least five functions. get_current_user depends on the token and the session, the session depends on the engine, and the order in which they run is not the order in which they appear.

The tree is built once, solved per request

When the decorator registers the route, FastAPI calls into its dependency utilities to inspect the endpoint signature. Every parameter whose default is a Depends(...) becomes a node holding the callable, its own sub-dependencies, and flags such as use_cache. This happens at import time. A malformed dependency fails at startup, not on the first request.

The resulting structure is a tree, not a list. A dependency can appear at several depths, and two different dependencies can share a third one.

Solve order is depth-first, not parameter order

For each request, FastAPI walks the tree and resolves children before their parent. That is the only order that works: get_current_user cannot run before get_db has produced a session. Parameter order in the signature is irrelevant to execution order. The tree also lets FastAPI reuse a child that has already been solved, which the next section covers.

from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session

app = FastAPI()

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

def get_current_user(db: Session = Depends(get_db)) -> User:
    return load_user(db)

@app.get("/me")
def read_me(user: User = Depends(get_current_user)):
    return user

If a dependency raises HTTPException, FastAPI stops resolving its dependants and returns the error response. Cleanup for dependencies that were already entered still runs, which is why try/finally around a yielded resource matters.

Caching and scopes

The default cache is per request and keyed by the callable, so calling Depends(get_db) in five places yields one session for that request. use_cache=False opts a specific declaration out and forces a fresh call each time. That is occasionally what you want, but it also means several sessions or clients inside one request, each needing its own cleanup.

Declaration Lifetime Executions per request Typical use
Lifespan-created client process zero, injected from state HTTP client, engine
Depends(get_db) request one, cached database session
Depends(get_db, use_cache=False) request one per declaration isolated unit of work
Depends(get_current_user) request one, cached identity and permissions
Sub-dependency of the above request one, cached shared lookups

App-scoped objects do not belong in a request dependency. An HTTP client or connection pool created once during startup is cheaper and safer than constructing one per request, provided it is safe to share across threads and tasks.

Generator dependencies and cleanup

A dependency written with yield is split into setup and teardown. FastAPI enters it through an async exit stack, passes the yielded value to dependants, and runs the code after yield when the request is finished. In current FastAPI versions this teardown runs after the response has been sent; older versions ran it before the response, so check the version you deploy if the distinction affects you. For streaming responses, teardown waits until the stream completes.

Teardown runs even when the endpoint raises, and even when a later dependency fails. If the teardown itself raises, the response may already have started, leaving the client with a truncated body and the server with a logged exception.

What changes under load

Dependency trees are cheap to solve and expensive to misuse. A cached session is one connection checked out for the whole request, including serialization and any awaits after the handler. A dependency that calls an external service on every request puts that service’s latency inside your request budget, before the endpoint starts. use_cache=False on a session dependency multiplies connections per request and can exhaust the pool under load. The tree is deterministic; the resource lifetimes are the part that needs design.

What to carry forward

Read a dependency as three facts: what it builds, how long it lives, and who else shares it. Keep app-scoped clients in the lifespan, request-scoped sessions behind yield, and caching on unless you can explain why a second instance is required. When a request is slow or a connection leaks, draw the tree and mark where each resource is created and destroyed.