Dependencies and async
Reusable dependencies with Depends, yield-based resource cleanup, and the blocking-call mistake that stalls an entire async worker.
Dependencies
from typing import Annotated
from fastapi import Depends, Header, HTTPException, Query, FastAPI
app = FastAPI()
def get_token(x_token: Annotated[str | None, Header()] = None) -> str:
if x_token != "s3cret":
raise HTTPException(status_code=401, detail="invalid token")
return x_token
class Pagination:
def __init__(self,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
offset: Annotated[int, Query(ge=0)] = 0):
self.limit, self.offset = limit, offset
PageDep = Annotated[Pagination, Depends(Pagination)]
# applied to the whole router, not one handler
@app.get("/notes", dependencies=[Depends(get_token)])
def list_notes(page: PageDep) -> dict[str, int]:
return {"limit": page.limit, "offset": page.offset}def get_db():
session = SessionLocal()
try:
yield session # everything after yield is teardown
finally:
session.close()
DbDep = Annotated[Session, Depends(get_db)]
@app.post("/notes", dependencies=[Depends(get_token)])
def create_note(note: NoteIn, db: DbDep):
db.add(to_row(note))
db.commit()- Dependencies are cached per request: the same callable requested twice runs once.
- Sub-dependencies compose — a dependency can itself declare
Dependsparameters. - Placement options: per-endpoint (
dependencies=[...]), per-router, or app-wide onFastAPI(dependencies=[...]). Dependsworks on any callable, including a class — the class signature becomes the injected schema.
async def versus def
import httpx, requests
@app.get("/slow-async")
async def slow_async():
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get("https://example.com/api") # yields to the loop
return r.json()
@app.get("/slow-sync")
def slow_sync():
# plain def runs in a threadpool, so this blocking call is safe here
return requests.get("https://example.com/api", timeout=10).json()
@app.get("/broken")
async def broken():
# BLOCKING call inside async def: the event loop cannot serve anyone else
return requests.get("https://example.com/api", timeout=10).json()| Handler | Runs on | Use when |
|---|---|---|
async def | Event loop | Every I/O call inside is await-based (httpx, asyncpg, motor) |
def | Threadpool (default 40 threads) | You call blocking libraries (requests, psycopg2, boto3, pandas) |
Blocking call in async def | Event loop, blocking it | Never — convert the call or drop the async |
⚠️
One blocking call inside an
async def handler freezes the event loop for every other request on that worker, including ones that are perfectly async. If a library has no async client, declare the handler as plain def and let FastAPI run it in the threadpool.Testing with overrides
from fastapi.testclient import TestClient
def fake_token() -> str:
return "test"
app.dependency_overrides[get_token] = fake_token
client = TestClient(app)
def test_list_notes():
r = client.get("/notes?limit=5")
assert r.status_code == 200
assert r.json() == {"limit": 5, "offset": 0}
app.dependency_overrides.clear() # always reset between testsOverriding a dependency swaps the auth, database or clock for the duration of a test without touching the endpoint code. Test the real get_token separately, and the handler logic here.
FAQ
How do I run background work after responding?
Use
BackgroundTasks for short, in-process jobs. For anything long, durable or retryable, enqueue it and let a separate worker process it — a background task still dies with the worker.Do dependencies run for every request even when unused?
No. A dependency runs only if the endpoint (or a router/app-level dependency) requires it, and its result is cached for that request.
Related
Pydantic request and response models Caching and conditional requests
Last refreshed 2026-09-18.