Mid-level 3 min · March 05, 2026

FastAPI Dependency Injection — How and Why to Use It

Master FastAPI's dependency injection system with Depends().

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • FastAPI Depends() declares functions that run before your route logic — framework handles resolution and injection
  • Dependencies enable Inversion of Control: you declare what an endpoint needs, FastAPI handles the how
  • yield dependencies act as context managers — setup before yield, teardown in finally, even on exceptions
  • Sub-dependencies nest recursively: Auth -> Role Check -> DB Lookup creates a validation graph
  • By default FastAPI caches dependency results per request — set use_cache=False for side-effect dependencies
  • Biggest mistake: copy-pasting auth checks into every route instead of centralizing with Depends()
Plain-English First

Think of dependency injection like a restaurant kitchen. The chef (your endpoint) does not go shopping for ingredients (auth, DB sessions, config). The kitchen manager (FastAPI) delivers exactly what the recipe calls for before cooking starts. If the ingredient is unavailable (auth fails), the order never reaches the chef. And if two dishes need the same ingredient, the manager sources it once and shares it across both — no duplication, no waste.

The yield pattern extends this analogy: after the meal is served, the kitchen manager also handles cleanup — washing the dishes, returning equipment to storage — even if something went wrong during cooking. You do not have to remember to clean up. The system guarantees it.

Basic Dependency — Shared Query Parameters

A dependency is just a standard Python callable — a function, a class, or anything with a __call__ method. By wrapping it in Depends(), you tell FastAPI to treat that callable's parameters as if they were declared directly on the endpoint. FastAPI introspects the signature, resolves the parameters from the incoming request (query string, headers, body), and injects the return value into your route handler.

This is the cleanest way to standardize pagination, filtering, sorting, or any shared input-processing logic across your entire API. The key insight is that the dependency function signature becomes part of the API contract — query parameters declared inside a dependency appear in the auto-generated OpenAPI schema exactly as if they were declared on the endpoint itself. API consumers see a consistent interface. Your codebase has one source of truth.

In production, this pattern eliminates an entire class of bugs: parameter drift across endpoints. Without DI, you copy the pagination logic into every route. Six months later, one endpoint caps limit at 100 and another allows limit=10000. One endpoint validates that skip is non-negative and another silently accepts skip=-5. The dependency pattern makes these inconsistencies structurally impossible — there is one function, one validation path, one default value. Changing it changes every endpoint that uses it simultaneously.

io/thecodeforge/dependencies/pagination.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from fastapi import FastAPI, Depends, Query, HTTPException, status
from typing import Annotated

app = FastAPI()


def common_params(
    skip: int = Query(default=0, ge=0, description="Number of records to skip"),
    limit: int = Query(default=100, ge=1, le=1000, description="Maximum records to return"),
) -> dict:
    """
    Standardized pagination parameters for all list endpoints.

    Enforces a hard cap at 1000 to prevent unbounded queries.
    ge=0 on skip prevents negative offsets that some ORMs handle
    unpredictably.

    This function is the single source of truth for pagination logic.
    Changing the cap or default here applies to every endpoint that
    uses it — no find-and-replace required.
    """
    return {"skip": skip, "limit": limit}


# Type alias for readability — used across multiple route files
PaginationParams = Annotated[dict, Depends(common_params)]


@app.get("/users")
def list_users(params: PaginationParams):
    """
    List users with standardized pagination.
    Query params 'skip' and 'limit' appear in OpenAPI docs
    even though they are declared inside common_params, not here.
    """
    # Business logic: fetch users using params["skip"] and params["limit"]
    return {"context": "users", "pagination": params}


@app.get("/orders")
def list_orders(params: PaginationParams):
    """
    List orders with the same pagination contract.
    If the cap changes from 1000 to 500, it changes here
    and applies to both /users and /orders automatically.
    """
    return {"context": "orders", "pagination": params}


@app.get("/products")
def list_products(params: PaginationParams, category: str | None = None):
    """
    Dependencies compose cleanly with route-level parameters.
    category is unique to this endpoint; skip and limit come from the dependency.
    Both appear in the OpenAPI schema.
    """
    return {"context": "products", "category": category, "pagination": params}
Dependency as a Contract
  • The dependency function signature IS the contract — its parameters become API inputs visible in OpenAPI docs
  • FastAPI validates dependency parameters with the same rules as route parameters — Query constraints, type coercion, required vs optional
  • The return value is injected into the endpoint as a typed argument — use Annotated for explicit type documentation
  • Multiple endpoints share one dependency — single source of truth for validation logic, defaults, and constraints
  • Changing the dependency changes every endpoint that uses it simultaneously — no copy-paste drift possible
Production Insight
Centralized pagination prevents the class of bug where one endpoint silently allows limit=100000 and hammers the database with an unbounded query.
Dependency parameters appear in OpenAPI schema automatically — consumers see a consistent, documented interface.
Rule: if two endpoints share identical parameter logic, extract it as a dependency. If three endpoints share it and one has a slightly different validation rule, that is a sign the rule should be standardized, not the sign to keep them separate.
Key Takeaway
Dependencies centralize shared parameter logic — one callable, many endpoints, one place to change. The dependency signature becomes part of the API contract and appears in OpenAPI documentation automatically. Extract when logic is shared across two or more endpoints; keep inline when it is genuinely unique. The goal is not maximum DI usage — it is eliminating duplication that creates drift.
When to Extract a Dependency
IfTwo or more endpoints share identical query parameter logic or validation rules
UseExtract to a dependency — single source of truth prevents drift and inconsistency across endpoints
IfParameter validation is complex — cross-field validation, range checks with business rules, conditional requirements
UseExtract to a dependency — centralize validation logic with clear, consistent error messages rather than duplicating conditionals in each route
IfParameter logic is simple and genuinely unique to one endpoint
UseKeep inline — do not over-abstract single-use parameters into a dependency that has only one caller
IfA shared parameter has different validation rules in different endpoints
UseStandardize the rule in the dependency — if the rules are legitimately different, question whether they should be different before creating two separate dependencies

Authentication Dependency & Logic Branching

Dependencies are the gatekeepers of your routes. By raising an HTTPException inside a dependency, you stop the request before it reaches your route handler. The route never executes, the business logic never runs, and the response is returned immediately with the error status code. This is not just convenient — it is a security property.

The authentication dependency pattern separates the concern of 'who is this request from' from 'what does this request do.' The endpoint never needs to know how authentication works. It does not parse tokens, query user tables, or check API key formats. It receives an authenticated identity object and operates on it. The auth mechanism is entirely contained in the dependency.

This separation has a direct and concrete security benefit: changing your auth mechanism requires changing one function. JWT to OAuth2, API keys to mutual TLS, single-tenant to multi-tenant — the endpoint code is untouched. Without DI, you would need to audit every route handler individually, and that audit will miss something. There is no version of 'update every route manually' that is reliably complete.

The second security benefit is placement. Applied at the router level, auth dependencies create a structural guarantee: every route in the group requires authentication. There is no way to add a new endpoint and forget the auth check — the router applies it automatically. Applied at the route level, auth is a per-developer discipline that fails the moment someone is moving fast and forgets.

io/thecodeforge/security/auth.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from fastapi import FastAPI, Depends, HTTPException, Header, status, APIRouter
from typing import Annotated
from dataclasses import dataclass


@dataclass
class AuthenticatedUser:
    """
    The identity object returned by a successful auth dependency.

    Returning a structured object instead of a raw dict makes
    downstream role checks type-safe and self-documenting.
    Mypy catches accesses to non-existent fields at development time
    rather than at runtime in production.
    """
    user_id: str
    role: str
    scopes: list[str]


def validate_api_key(
    x_forge_token: Annotated[str | None, Header()] = None,
) -> AuthenticatedUser:
    """
    Validate the X-Forge-Token header against the allowed key set.

    In production, replace the hardcoded check with a lookup against
    HashiCorp Vault, AWS Secrets Manager, or a secrets database.
    Never hardcode secrets in source — this example uses a literal
    only to show the dependency structure.

    Raises HTTPException 401 if the token is missing or invalid.
    Returns an AuthenticatedUser if valid — the route receives the
    identity, not a boolean.
    """
    if not x_forge_token or x_forge_token != "forge-prod-secret":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or missing Forge API Key",
            headers={"WWW-Authenticate": "ApiKey"},
        )
    # In production: look up the key, fetch the associated user and scopes
    return AuthenticatedUser(
        user_id="svc-order-service",
        role="service",
        scopes=["orders:read", "orders:write"],
    )


def require_admin(
    auth: Annotated[AuthenticatedUser, Depends(validate_api_key)],
) -> AuthenticatedUser:
    """
    Sub-dependency for admin-only endpoints.

    Composes on top of validate_api_key — the request must first
    pass the base auth check, then pass the role check.
    FastAPI caches the validate_api_key result, so the token is
    only validated once even though two dependencies reference it.
    """
    if auth.role != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=f"Admin role required. Caller role: {auth.role}",
        )
    return auth


# Protected router — every route inherits auth at the router level
# No individual route can skip auth by forgetting the Depends() annotation
protected_router = APIRouter(
    prefix="/api/v1",
    dependencies=[Depends(validate_api_key)],
)

# Admin router — sub-dependency composes on the base auth check
admin_router = APIRouter(
    prefix="/admin",
    dependencies=[Depends(require_admin)],
)


@protected_router.get("/orders")
def list_orders(auth: Annotated[AuthenticatedUser, Depends(validate_api_key)]):
    """
    The route receives the identity object directly.
    It knows WHO is calling, not just that auth passed.
    This enables per-user data filtering without a second DB lookup.
    """
    return {
        "caller": auth.user_id,
        "scopes": auth.scopes,
        "orders": [],  # Fetch filtered by auth.user_id in production
    }


@admin_router.get("/metrics")
def get_internal_metrics(auth: Annotated[AuthenticatedUser, Depends(require_admin)]):
    """
    Only admin-role callers reach this handler.
    require_admin handles both auth and role — the route is clean.
    """
    return {"metrics": "[REDACTED]", "caller": auth.user_id}
Auth Dependency Pitfalls — and the Security Holes They Create
  • Never put auth logic inside the route handler — it will be missing in some endpoints, always, eventually
  • Raising HTTPException in a dependency stops the request before the route executes — this is the mechanism, not a side effect
  • Return the authenticated identity object from the dependency, not a boolean — the route needs to know WHO, not just IF
  • Use sub-dependencies for role-based access: require_admin depends on validate_api_key, which is cached and not re-executed
  • Apply auth at the router level — per-route auth is a discipline that fails under deadline pressure and code review gaps
Production Insight
One endpoint without the auth dependency is a security hole that survives for months. Code review catches logic errors; it does not reliably catch missing Depends() annotations when the endpoint looks structurally complete without it.
Router-level dependencies are structural enforcement — they make the secure path the only path for every route in the group.
Rule: apply auth at the router level. If you have a mix of public and protected routes, create separate routers. Mixing protected and unprotected routes in the same router always produces gaps eventually.
Key Takeaway
Auth dependencies gate the request — HTTPException in a dependency stops execution before the route runs, with no partial execution. Apply auth at the router level to make security structural rather than disciplinary. The dependency returns the identity object, not a boolean — the route needs to know who is calling to filter data, log the caller, and enforce data ownership boundaries.
Auth Dependency Placement Decision Tree
IfAll routes in a router require the same authentication check
UseApply Depends(validate_api_key) at the router level via APIRouter(dependencies=[...]). Every route in the group inherits the check structurally — no per-route annotation required.
IfSome routes are public and some require authentication in the same module
UseCreate two routers: a public router with no auth dependency and a protected router with the auth dependency. Register both on the app. Never mix public and protected routes in the same router.
IfDifferent routes need different auth levels — user, admin, service-to-service
UseUse sub-dependencies: require_admin depends on get_current_user, then checks role. FastAPI caches get_current_user, so the token is validated once regardless of how many role checks compose on top of it.
IfA route needs to access the identity AND enforce a role check
UseDeclare only the highest-level sub-dependency (require_admin). It returns the AuthenticatedUser after the role check — the route receives the identity without needing a separate Depends(get_current_user).

The 'Yield' Pattern: Database Session Management

Managing database connections is where FastAPI's dependency injection either saves you or burns you. You must open the session before the route runs, and you must close it — return it to the pool — after the route completes, regardless of whether the route succeeded, raised an HTTPException, or crashed with an unhandled error. 'Regardless of outcome' is the hard part.

FastAPI's yield dependencies solve this by acting as request-scoped context managers. Code before the yield statement runs during setup — opening the session, acquiring any necessary locks, initializing state. Code after the yield in a finally block runs during teardown — closing the session, releasing locks, cleaning up. FastAPI holds a reference to the generator across the entire request lifecycle and resumes it after the response is sent or the exception is handled.

The critical distinction — the one that causes the most production incidents — is between yield dependencies and plain function dependencies with try/finally blocks. In a plain function dependency, the finally block does not execute when the route raises an HTTPException. FastAPI intercepts the exception at the route execution boundary and converts it to a JSON error response. This interception happens before control returns to the plain function's finally block. The database session is never closed. The connection leaks.

Only yield dependencies get guaranteed teardown because FastAPI explicitly resumes the generator after exception handling. The generator is paused at the yield, the exception is handled, the response is sent, and then FastAPI calls next() on the generator to trigger the finally block. This is not automatic — it is a deliberate design in FastAPI's dependency execution model. If you are acquiring any resource in a dependency — database session, HTTP client, file handle, distributed lock — you must use yield. There is no production-safe alternative.

io/thecodeforge/database/session.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy import create_engine, event, text
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import QueuePool
from typing import Annotated, Generator
import logging

logger = logging.getLogger(__name__)

SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/forge_db"

engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    poolclass=QueuePool,
    pool_size=20,         # Base connections kept open
    max_overflow=10,      # Additional connections allowed under burst load
    pool_recycle=300,     # Recycle connections after 5 minutes to prevent stale state
    pool_pre_ping=True,   # Test connection health at checkout — catches dropped connections
    pool_timeout=30,      # Raise after 30s waiting for a connection — fail fast, not hang
)

SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine,
)


def get_db() -> Generator[Session, None, None]:
    """
    Yield a database session for the duration of a single request.

    MUST use yieldnot return.

    With yield: FastAPI holds the generator open across the request
    lifecycle. After the route completes (or raises), FastAPI resumes
    the generator, triggering the finally block that closes the session.

    Without yield (plain try/finally): FastAPI's exception handling
    intercepts HTTPException before the finally block executes.
    The session is never closed. The connection leaks. The pool
    exhausts. The service returns 503.

    This is the most common FastAPI production bug — and the most
    preventable one.
    """
    db = SessionLocal()
    logger.debug(f"DB session opened: id={id(db)}")
    try:
        # Setup phase: session is open and ready
        yield db
        # If the route commits explicitly, nothing to do here
        # If the route did not commit, nothing is committed — no surprise writes
    except Exception:
        # Roll back any uncommitted transaction before closing
        # Prevents partial writes from surviving an exception
        db.rollback()
        raise
    finally:
        # Teardown phase: ALWAYS runs — success, HTTPException, or crash
        logger.debug(f"DB session closed: id={id(db)}")
        db.close()


# Type alias — use this in route signatures for readability
DBSession = Annotated[Session, Depends(get_db)]


app = FastAPI()


@app.get("/db-status")
def check_db_connection(db: DBSession):
    """
    Health check that validates the DB session is functional.
    Uses db.execute() to confirm the connection is active,
    not just that the session object was created.
    """
    try:
        db.execute(text("SELECT 1"))
        return {"status": "connected", "session_id": id(db)}
    except Exception as e:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Database unreachable: {str(e)}",
        )


@app.get("/orders/{order_id}")
def get_order(order_id: int, db: DBSession):
    """
    Route that uses the injected session for a real query.
    Whether this raises 404, 422, or succeeds — the session is closed.
    FastAPI guarantees teardown via the yield generator resume.
    """
    # order = db.query(Order).filter(Order.id == order_id).first()
    # if not order:
    #     raise HTTPException(status_code=404, detail="Order not found")
    # return order
    return {"order_id": order_id, "status": "fetched", "session_id": id(db)}


# Example of what NOT to do — shown explicitly so the bug is recognizable in review:
# def get_db_wrong():
#     db = SessionLocal()
#     try:
#         return db  # Returns immediately — FastAPI has no generator to resume
#     except Exception:
#         db.close()
#         raise
#     finally:
#         db.close()  # This finally block DOES NOT RUN when the route raises HTTPException
#                     # FastAPI intercepts the exception before returning here
#                     # Connection leaks. Pool exhausts. 503s at moderate load.
Yield vs Plain Function — The Teardown Guarantee That Changes Everything
  • yield dependency: setup before yield, teardown in finally — guaranteed to run even on unhandled route exceptions
  • Plain function dependency: try/finally does NOT survive FastAPI's HTTPException interception at the route boundary
  • This asymmetry is the #1 cause of database connection pool exhaustion in FastAPI production services
  • Add db.rollback() before db.close() in the except block — uncommitted transactions should not survive an exception
  • Monitor pool utilization with a Prometheus metric — alert at 80% of pool_size to catch leaks before exhaustion causes user-visible 503s
Production Insight
A plain try/finally dependency leaks one connection per failed request. With 20 connections in the pool and one validation error per second, you get 20 seconds of operation before exhaustion. The math is unforgiving.
pool_pre_ping=True is the second most important pool configuration — without it, stale connections that survived across pool_recycle boundaries cause mid-request failures that look like application bugs but are infrastructure issues.
Rule: any dependency that acquires a resource uses yield. No exceptions to this rule. Code review should reject any resource-acquiring dependency that does not use yield.
Key Takeaway
yield dependencies are request-scoped context managers for resources. Only yield guarantees teardown on exceptions — plain try/finally does not survive FastAPI's exception handling boundary. Every resource-acquiring dependency must use yield. This is not a recommendation; it is a correctness requirement for production stability. A service that leaks connections under load will always fail — the only question is how long until it does.
Yield Dependency Decision Tree
IfDependency acquires a resource — database session, HTTP client, file handle, distributed lock, external connection
UseUse yield — teardown must be guaranteed even on exceptions. Add rollback() for transactional resources before close().
IfDependency is pure computation — parse and validate a JWT, extract query parameters, check a header format
UsePlain function is appropriate — there is no resource to release, and yield adds complexity without benefit.
IfDependency needs to run cleanup after the response is sent — emit a metric, write an audit log, record request duration
UseUse yield — code after yield runs after the response is sent. The cleanup happens without blocking the response to the client.
IfMultiple routes need isolated sessions — each request must have its own transaction boundary
UseUse yield dependency with per-request SessionLocal() creation. Never share a session across requests — sessions are not thread-safe and hold transaction state.
● Production incidentPOST-MORTEMseverity: high

Database Connection Pool Exhaustion from Missing Yield in Dependency

Symptom
The order service started returning 503 errors after roughly 20 minutes of traffic. Logs showed 'QueuePool limit of size 20 overflow 0 reached, connection timed out, timeout 30.0'. Restarting the pod cleared the symptom temporarily, but the errors returned within 20 minutes. Increasing pool_size to 50 bought more time before the same failure — which confirmed this was a leak, not an undersized pool.
Assumption
The database was overloaded or the connection pool size was too small. The initial response was to increase pool_size and add a read replica. Neither fixed the issue because the root cause was not pool capacity — it was connection release.
Root cause
The get_db dependency was written as a regular function with manual try/except/finally, not as a yield dependency. When a route raised an HTTPException — the most common case being 422 validation errors on malformed request bodies — the exception propagated through FastAPI's exception handling layer before db.close() in the dependency's finally block could execute. FastAPI intercepts HTTPException at the route execution level and converts it to a JSON response. This interception happens before control returns to the dependency's finally block in a plain function. Only yield dependencies get a guaranteed callback after the route completes or raises — because FastAPI holds a reference to the generator and explicitly resumes it after the response is handled. With a pool of 20 connections and roughly one validation error per second, the pool exhausted in approximately 20 seconds. Each failed request left one connection open, held by a session object that was never closed. The connection eventually timed out and was returned to the pool — but by then, the queue of waiting requests had already triggered a cascade of 503s.
Fix
Converted get_db from a regular function to a yield dependency. FastAPI now holds the generator open across the request lifecycle and guarantees the finally block executes after the response is sent, regardless of whether the route succeeded or raised. Added a Prometheus metric tracking active connection count with an alert threshold at 80% of pool_size. Set pool_size=20, max_overflow=10, pool_recycle=300, pool_pre_ping=True to handle stale connections that survive across pool_recycle boundaries.
Key lesson
  • Always use yield for resource-managing dependencies — plain try/finally does not survive FastAPI's exception handling interception
  • FastAPI only guarantees teardown for yield dependencies — the generator resume is the mechanism, not the try/finally block
  • Increasing pool_size to fix exhaustion without diagnosing the leak treats the symptom — the pool will always exhaust eventually if connections are not being released
  • Monitor connection pool utilization with a dedicated metric — alert at 80% so you catch leaks before they cause user-visible failures
  • Set pool_pre_ping=True to detect stale connections at checkout rather than failing mid-request
Production debug guideCommon symptoms of dependency misconfiguration — with diagnosis steps, not just symptoms4 entries
Symptom · 01
Database connection pool exhausted — QueuePool limit reached errors under load, service recovers after pod restart but degrades again within minutes
Fix
The first thing to check is whether get_db (or any resource-acquiring dependency) uses yield. Without yield, route exceptions skip db.close() because FastAPI's exception handling intercepts before the dependency's finally block runs. Convert to yield dependency immediately. Then add a connection count metric to verify the fix — if active connections stabilize below pool_size after the change, the diagnosis was correct.
Symptom · 02
Dependency runs twice per request — side effects duplicated, counters incremented twice, unique IDs generated twice
Fix
Two causes: either the same dependency is referenced in multiple places in the dependency graph without use_cache=False triggering re-execution (which would be a bug — the default is to cache), or use_cache=False was set intentionally but the dependency is shared across two sub-dependencies that both need independent executions. Audit the dependency graph by adding logging inside the function. If it is called twice when it should be cached, check for duplicate Depends() declarations at different levels.
Symptom · 03
Auth dependency validates successfully but the route still returns 401 or 403 — token is correct, user is authenticated, but access is denied
Fix
Verify the dependency is applied at the correct scope. Router-level dependencies compose with route-level ones — they do not override them. If your router applies a base auth check and the route applies an additional role check, both must pass. Also verify the dependency returns the identity object and not just a truthy value — if the route extracts a field from the returned object and that field is missing, you get an unexpected access failure that looks like an auth failure.
Symptom · 04
Dependency override not working in tests — real database is being hit, real auth service is being called, mock is ignored
Fix
Overrides must be set before the TestClient makes its first request. If you set app.dependency_overrides[get_db] = mock_db after TestClient(app) is already created, the override applies to subsequent requests — but if the client constructor triggers any startup events that call the dependency, those calls use the original. Set overrides before TestClient instantiation. After each test, call app.dependency_overrides.clear() to prevent state from leaking into the next test.
★ FastAPI DI Debug Quick ReferenceFast diagnostic commands for dependency injection issues — ordered by frequency in production
Connection pool exhaustion — requests timing out on DB access, QueuePool limit errors in logs
Immediate action
Check active connection count and verify the yield dependency pattern is in place
Commands
SELECT count(*), state FROM pg_stat_activity WHERE datname = 'your_db' GROUP BY state;
grep -c 'QueuePool limit' /var/log/app.log
Fix now
Convert get_db to yield dependency. If active connections in pg_stat_activity exceed pool_size and keep growing under constant load, the leak is confirmed. The yield fix stops the growth immediately — connections will stabilize at the concurrent request count.
Dependency not injecting expected value — endpoint receives None or wrong type despite dependency appearing to succeed+
Immediate action
Inspect the dependency return type and the Annotated type annotation on the endpoint parameter
Commands
print(app.routes) # Verify route dependency declarations are present
# Add explicit logging inside the dependency to confirm execution and return value: def get_db(): logger.info('get_db called — creating session') db = SessionLocal() try: logger.info(f'get_db yielding session id={id(db)}') yield db finally: logger.info(f'get_db closing session id={id(db)}') db.close()
Fix now
Verify the Annotated type on the endpoint parameter matches what the dependency yields. A type mismatch does not raise an error — FastAPI injects whatever the dependency returns, and if the type hint is wrong, mypy catches it but the runtime silently injects the wrong type.
Test dependency override not taking effect — real services called, real database hit despite mock configuration+
Immediate action
Verify the override is configured before TestClient is instantiated and the correct function reference is used as the key
Commands
app.dependency_overrides[get_db] = lambda: mock_session # Must be the exact same function object
# Clear after each test to prevent state leakage: def teardown(): app.dependency_overrides.clear()
Fix now
The override key must be the exact function object imported from the module — not a re-import or a copy. If your app imports get_db from app.database and your test imports get_db from a different path, they may be different objects even if the code is identical. Use a single import source.
Slow requests — p99 latency much higher than business logic execution time, dependency chain suspected+
Immediate action
Check whether independent dependencies are nested (sequential execution) versus flat (parallel resolution with caching)
Commands
# Anti-pattern: D executes twice because B and C both declare Depends(D) with use_cache=False # A depends on B (which depends on D), A depends on C (which depends on D) # D runs once for B, once for C — sequential, not cached
# Correct pattern: D executes once, result cached and shared # A depends on B, A depends on C, both B and C depend on D # FastAPI resolves D first, caches result, injects into both B and C
Fix now
FastAPI caches sub-dependency results per request by default. Ensure use_cache is not accidentally set to False on shared sub-dependencies. Flatten the graph so shared sub-dependencies are siblings, not duplicated descendants. Use OpenTelemetry spans inside yield dependencies to measure setup and teardown timing independently.
FastAPI Dependency Types: Complete Comparison
Dependency TypeTeardown GuaranteeUse CaseTrade-off
Plain function Depends()No — skipped on route exceptions due to FastAPI exception interceptionPure computation: parse token, validate params, extract query args, check header formatNo cleanup mechanism — do not use for any resource acquisition, no matter how minor
Yield Depends()Yes — finally block runs even on unhandled exceptions, after response is sentResource management: DB sessions, HTTP client connections, file handles, distributed locksSlightly more complex lifecycle — must understand setup/teardown phases and where exceptions fall
Class-based Depends()No (unless __call__ uses yield internally)Stateful logic: service objects, complex configuration, multi-method validators, dependency factoriesMore verbose and requires understanding of Python callable protocol — worth it for complex reusable services
Sub-dependency (nested Depends)Inherited from the inner dependency type — yield inside sub-dependency is guaranteedComposed validation chains: Auth -> Role Check -> Permission Lookup -> DB SessionDeep nesting beyond 3 levels becomes hard to trace during debugging — flatten where possible
Router-level dependencyInherited from dependency type — applies to every route in the routerStructural enforcement: all routes get auth, rate limiting, request ID injection, loggingCannot exempt individual routes without creating a separate router — plan router boundaries during design
Global dependency (app level)Inherited from dependency type — runs on every single requestCross-cutting concerns: CORS preflight handling, global request ID injection, request timingRuns on every request including health checks and static file serving — add path exclusions for non-API routes

Key takeaways

1
Depends() is the primary tool for Inversion of Control in FastAPI
it decouples infrastructure concerns (auth, sessions, config) from business logic in route handlers, making both independently testable and independently changeable.
2
Dependency caching
FastAPI reuses the first result of each dependency within a single request by default. Shared sub-dependencies like database sessions are resolved once regardless of how many places in the dependency graph reference them — use use_cache=False only when independent execution per reference is intentionally required.
3
The yield syntax provides guaranteed setup and teardown for request-scoped resources
code before yield runs during setup, code in the finally block runs after the response is sent or the exception is handled. This guarantee does not exist for plain function dependencies, making yield mandatory for any dependency that acquires a resource.
4
Sub-dependencies compose cleanly
Auth -> Role Check -> DB Lookup creates a validation graph where each layer can be tested independently and changed without touching the others. FastAPI handles topological ordering and caching automatically.
5
Router-level dependencies create structural enforcement
every route in the group inherits the dependency without per-route annotation. Apply auth and cross-cutting concerns at the router level; per-route annotation is a maintenance discipline that fails under deadline pressure.

Common mistakes to avoid

5 patterns
×

Using a plain function instead of yield for database session dependencies

Symptom
Connection pool exhaustion under moderate load — QueuePool limit reached errors, service recovers after pod restart but degrades again within minutes. The pool size looks adequate on paper but cannot handle the actual request rate because connections are never being released.
Fix
Convert get_db to a yield dependency. FastAPI resumes the generator after the response is sent or the exception is handled, which triggers the finally block. Add db.rollback() in an except clause before db.close() to prevent uncommitted transactions from surviving exceptions. Add pool utilization monitoring with an alert at 80% of pool_size — if the fix is working, the metric stabilizes at the concurrent request count instead of growing monotonically.
×

Applying auth dependencies at the route level instead of the router level

Symptom
A new endpoint is added under a protected path prefix without the Depends(auth) annotation — it becomes publicly accessible. The gap is discovered in a security audit months later, after the endpoint has been in production. Because the other routes in the same file all have the annotation, the missing one looks like an intentional omission rather than a bug.
Fix
Apply authentication dependencies at the router level: APIRouter(prefix='/api/v1', dependencies=[Depends(validate_api_key)]). Every route registered on that router inherits the dependency structurally — there is no annotation to forget. Create separate routers for public and protected routes. Never mix public and protected routes in the same router, even if it feels like unnecessary structure early in the project.
×

Not using use_cache=False for dependencies with side effects that must run independently each time they are referenced

Symptom
A dependency that generates a unique request correlation ID runs once per request as expected, but when two sub-dependencies both reference it, the ID is generated once and shared — which is correct for correlation IDs but wrong for dependencies that should produce independent values like idempotency keys or random nonces.
Fix
Set Depends(generate_nonce, use_cache=False) to force independent execution on each reference. By default, FastAPI caches dependency results for the entire request — this is the right behavior for expensive shared dependencies like DB sessions and wrong for dependencies that must produce unique values per reference. Document which dependencies use use_cache=False and why — it is non-obvious to future readers.
×

Deep nesting of sub-dependencies creating an untraceable execution graph that is impossible to reason about during incident response

Symptom
A route with simple business logic has p99 latency significantly higher than the sum of its query times. Tracing shows time disappearing between route entry and first query. The dependency chain is 6 levels deep with repeated sub-dependency calls that are not being cached because of incorrect graph structure.
Fix
Flatten the dependency graph. If A depends on B and C, and both B and C depend on D, ensure D is a sibling in the graph (A depends on D, B depends on D, C depends on D) so FastAPI caches D once and injects it into both B and C. Use app.dependency_overrides in development to inject a tracing wrapper around each dependency. Add OpenTelemetry spans inside yield dependencies to measure setup and teardown timing independently from business logic.
×

Dependency override not working in integration tests — real services called, real database hit, mock is ignored despite being configured

Symptom
Tests hit the real database despite app.dependency_overrides[get_db] = mock_db being configured. Some tests pass and some fail depending on test execution order, suggesting the override is sometimes active and sometimes not.
Fix
Set app.dependency_overrides[original_func] = mock_func before creating the TestClient — overrides are resolved at request time, not at app startup, but the TestClient must be created after the override is in place. Use the exact function object as the key — if your app imports get_db from app.database and your test imports it from a different path, they may be different objects. Clear overrides after each test with app.dependency_overrides.clear() to prevent state leakage between tests in the same session.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Describe the 'Dependency Graph' resolution in FastAPI. How does it handl...
Q02SENIOR
How does FastAPI ensure that a 'yield' dependency cleans up resources if...
Q03SENIOR
Scenario: You need to implement a 'Soft-Delete' filter globally. How wou...
Q04JUNIOR
What are 'Security Dependencies' in FastAPI, and how do they integrate w...
Q05SENIOR
How would you override a dependency during an integration test to avoid ...
Q01 of 05SENIOR

Describe the 'Dependency Graph' resolution in FastAPI. How does it handle a scenario where Endpoint A depends on B and C, while both B and C depend on D?

ANSWER
FastAPI resolves the dependency graph as a directed acyclic graph (DAG) using topological ordering, with per-request caching as the key optimization. When Endpoint A declares Depends(B) and Depends(C), FastAPI builds the full dependency graph before executing anything. It identifies that D is a shared dependency referenced by both B and C. On the first resolution — let's say for B — FastAPI resolves D, executes it, and stores the result in a per-request cache keyed by the function object. When C needs D, FastAPI returns the cached result without re-executing D. This caching behavior is the default (use_cache=True on Depends). It ensures that expensive sub-dependencies — database sessions in particular — are created exactly once per request regardless of how many places in the dependency graph reference them. If D must run independently for each reference — for example, D generates a unique value or has a side effect that must not be shared — you declare it as Depends(D, use_cache=False). In that case, D executes once for B and once for C as separate calls. The practical implication: structure your dependency graph so shared sub-dependencies are siblings, not duplicated descendants. FastAPI handles the caching automatically, but only if the graph structure allows it to identify the shared reference.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between Depends() and a regular function call?
02
When should I use use_cache=False in a dependency?
03
Can I use dependencies for Class-based logic?
04
How do I debug which dependencies are being called and in what order?
🔥

That's Python Libraries. Mark it forged?

3 min read · try the examples if you haven't

Previous
FastAPI Response Models and Status Codes
40 / 51 · Python Libraries
Next
FastAPI Authentication — JWT and OAuth2 with Password Flow