FastAPI Dependency Injection Failures: A Diagnostic Guide
A diagnostic guide to FastAPI dependency injection failures: how to spot a missing Depends marker, a called dependency, leaked test overrides, and blocking async dependencies.
01 Feb 2026, 09:18 UTC

What the failure actually looks like
A route handler raises TypeError or AttributeError because an argument that was supposed to be a dependency is None, or the request returns HTTP 422 or 500 even though the dependency function itself looks fine. The traceback usually points at the line inside the handler that uses the value, not at the parameter declaration — which is why the cause is easy to miss.
The useful takeaway: in FastAPI a parameter is only a dependency when it is marked with Depends (or Security). Without that marker, FastAPI treats the parameter as request data — a query parameter, path parameter, header, or JSON body — and your dependency function is never called.
Cause and symptom table
| Cause | What you observe |
|---|---|
Parameter has no Depends marker | Scalar type becomes an optional query parameter and is None; a Pydantic model becomes a JSON body and returns 422 when absent |
Depends(get_db()) — the callable was called | The dependency runs once at import time; the handler receives its return value, or import fails outright |
Blocking I/O inside an async def dependency | No exception; requests slow down or time out under concurrency and look like intermittent resolution failures |
A stale app.dependency_overrides entry | Correct in production, wrong value in tests only |
| Circular import between dependency modules | ImportError at startup, before any request is served |
Note that a synchronous dependency is not a bug. Starlette runs def dependencies in a threadpool automatically. The problem is blocking work inside an async def dependency, which stalls the event loop.
Ordered checks
- Read the traceback bottom-up. Find the last frame that belongs to your code. If the failing line indexes or calls the dependency value, the value is
Noneand the declaration is the suspect. - Confirm the marker. Open
/docson a running instance. If the parameter appears as a query parameter in Swagger UI,Dependsis missing. This is faster and more reliable than reading the source. - Check for a call instead of a reference. Search the codebase for
Depends(followed by a name and a closing parenthesis, e.g.Depends(get_session()). The correct form passes the callable itself. - Check test overrides. Grep for
dependency_overrides. An override set in one test and never cleared leaks into later tests and produces wrong-but-not-erroring values. - Check versions. Run
pip show fastapi starlette pydanticin the same environment that serves the app. Mismatched pins between a local shell and a container are a common source of behaviour that does not reproduce.
Fixes tied to each finding
- Missing marker: declare the dependency explicitly, preferably with the
Annotatedform used in current FastAPI versions:settings: Annotated[dict, Depends(get_settings)]. - Called dependency: remove the parentheses so FastAPI resolves it per request. If you genuinely want a module-level singleton, build it outside the dependency and have the dependency return it.
- Blocking async dependency: move blocking calls into a
defdependency, or wrap them withawait run_in_threadpool(...)fromstarlette.concurrency. - Leaked override: clear overrides in a fixture teardown or a
try/finallyblock around the test. - Circular import: move the shared dependency into its own module that imports nothing from the routers, then import it from both sides.
A concrete example
The two routes below differ only in the parameter declaration. Run the file with uvicorn app:app --reload from the project root, then request both paths.
# app.py
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_settings() -> dict:
return {"region": "eu-west-1"}
Settings = Annotated[dict, Depends(get_settings)]
@app.get("/ok")
def ok(settings: Settings):
return {"region": settings["region"]}
@app.get("/broken")
def broken(settings: dict = None): # no Depends: FastAPI sees a query param
return {"region": settings["region"]}
Expected behaviour: /ok returns {"region": "eu-west-1"} with status 200, and /broken raises TypeError because settings is None. In /docs, settings appears as a query parameter on /broken and does not appear at all on /ok. These examples have not been executed here; treat the expected values as the contract to check, not as observed output.
Verifying the fix
A request-level test is the cheapest confirmation. Recent FastAPI and Starlette versions build TestClient on httpx, so confirm all three packages are installed in the same environment.
# test_app.py — run with: pytest test_app.py
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
def test_ok():
response = client.get("/ok")
assert response.status_code == 200
assert response.json() == {"region": "eu-west-1"}
For the fix to count as verified, three things should hold: the endpoint returns 200 with the expected payload, the dependency appears in the OpenAPI schema only where intended, and the traceback no longer points into your handler. Do not rely on a specific log string — FastAPI's wording around dependency resolution differs between releases.
When to escalate
- The failure only appears under concurrency. Re-run with a single worker (
uvicorn app:app) and compare; a difference points at shared state or event-loop blocking rather than at resolution. - The failure appeared immediately after a FastAPI, Starlette, or Pydantic upgrade. Pin the previous version to confirm the regression, then read the changelog for that range.
- You need the resolved dependency tree.
route.dependantexposes it, but this is an internal attribute that is not part of the documented API and can change between releases — use it for local inspection only, never in application code. - You are filing an issue. Reduce it to one file, one route, one dependency, and state the exact versions from
pip show.
Limitations
This guide covers resolution failures at the parameter level. It does not cover authentication flows built on Security, WebSocket dependencies, or lifespan-managed resources, all of which have their own failure modes. Dependency caching also matters: FastAPI resolves a given dependency once per request by default, so if you need a fresh instance within one request, pass use_cache=False to Depends rather than assuming the callable runs again. Verify any behaviour you depend on against the FastAPI version pinned in your own environment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.