What this usually means
FastAPI's dependency injection system inspects the type signature of your dependency callables. If you define a dependency with `def` but the endpoint is `async def`, FastAPI by default tries to run the dependency in a thread pool via `run_in_executor`. This works for simple cases but breaks when the dependency returns a generator or uses context managers. More critically, if the dependency itself is not a proper callable (e.g., you pass an instance instead of a class, or the function is not defined with `def`/`async def`), FastAPI will raise an error. Another class of bugs involves the `Depends` scope: if you reuse the same dependency object across different endpoints, FastAPI may cache the result incorrectly, leading to stale data or connection leaks.
The first ten minutes — establish facts before touching code.
- 1Check if endpoint is `async def` and dependency is `def` – if yes, switch dependency to `async def` or use `run_sync=False`
- 2Run `curl -v http://localhost:8000/docs` and see if the OpenAPI schema loads; if not, the DI error happens at startup
- 3Add `print(type(dep))` before the endpoint call to verify the dependency is a callable, not an instance
- 4Set `log_level='debug'` in Uvicorn and look for lines containing 'dependency' or 'Depends' in the startup logs
- 5If using a generator dependency (e.g., database session), ensure it's defined with `async def` and `yield`
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe endpoint definition file – verify the `Depends()` argument is a function or class, not a call like `Depends(get_db())`
- searchThe dependency function itself – check for `async def` vs `def` and its return type annotation
- searchStarlette's `run_in_executor` code path if you see thread pool errors
- searchFastAPI's `dependency_overrides` dict if you're using test overrides – wrong key type
- searchUvicorn startup logs (stderr) – DI errors often surface during route registration
- searchThe `__init__` of a class dependency – if it takes arguments, FastAPI won't instantiate it automatically
Practical causes, not theory. These are the things you will actually find.
- warningPassing an instance of a class to `Depends` instead of the class itself (e.g., `Depends(my_instance)` instead of `Depends(MyClass)`)
- warningDefining a dependency as a synchronous generator (`def` with `yield`) in an async endpoint – must be `async def`
- warningMissing `async` keyword on a dependency that uses `await` internally (e.g., `await db.commit()`)
- warningType mismatch: dependency returns a dict but endpoint expects a Pydantic model, causing FastAPI to raise an internal error
- warningCircular dependency: Depends(A) -> Depends(B) -> Depends(A) without using `@lru_cache` or proper refactoring
- warningUsing `Depends` with a lambda or partial function – FastAPI requires a named callable for introspection
Concrete fix directions. Pick the one that matches your root cause.
- buildChange `def get_db()` to `async def get_db()` and `yield` to `async yield` (Python 3.6+)
- buildReplace `Depends(MyClass())` with `Depends(MyClass)` and let FastAPI instantiate it
- buildIf you must pass arguments, wrap the class in a factory function: `def get_dep(): return MyClass(arg)` and use `Depends(get_dep)`
- buildUse `from fastapi import Depends` and never shadow the name `Depends` with a variable
- buildFor test overrides, ensure the override function has the exact same signature (including async) as the original
A fix you cannot prove is a guess. Close the loop.
- verifiedCall the endpoint with `curl` and confirm a 200 response instead of 500
- verifiedAdd a `try/except` around the dependency call in a test and assert no exception is raised
- verifiedCheck the OpenAPI schema at `/docs` – it should load without errors and show the dependency parameters
- verifiedRun the app with `--reload` and watch the terminal for any startup errors
- verifiedWrite a unit test that directly calls the dependency function and asserts it returns the expected type
Things that make this bug worse or harder to find.
- warningDo not use `Depends` inside a list or dict comprehension – it must be a top-level argument of the endpoint
- warningNever call `Depends()` with a result of a function call – e.g., `Depends(get_db())` will try to use the return value as a dependency
- warningAvoid mixing `def` and `async def` in the same dependency chain unless you understand the thread pool implications
- warningDon't ignore the error message – FastAPI's DI errors are usually explicit; read the full traceback
- warningDo not set `dependency_overrides` to a lambda unless you also set `use_cache=False` appropriately
Production DB Session Leak Due to Sync Dependency in Async Endpoint
Timeline
- 08:45Deployed new endpoint /orders/{order_id} for order details
- 09:10PagerDuty alert: endpoint returns 500 for 70% of requests
- 09:12Checked logs: 'RuntimeError: cannot use sync dependency in async handler without run_sync' but only intermittently
- 09:20Found the dependency `get_db` was defined as `def get_db()` with `yield` (synchronous generator)
- 09:22Changed `get_db` to `async def get_db()` and `yield` to `async yield`
- 09:25Redeployed and endpoint returned 200 consistently
- 09:30Monitored for 30 minutes: no further errors, DB connection pool stable
I had written a new endpoint to fetch order details. The database session dependency was a simple generator: `def get_db(): db = Session(); yield db; db.close()`. I had used this pattern in dozens of synchronous endpoints without issue. But this new endpoint was `async def get_order(...)` because it called an external API. FastAPI tried to run the sync generator in a thread pool, which worked sometimes but caused intermittent failures when the generator's cleanup ran in the wrong thread.
The logs were cryptic: 'RuntimeError: cannot use sync dependency in async handler without run_sync'. I initially thought it was a concurrency bug. I spent 20 minutes adding locks and changing thread pool sizes. Then I noticed the error only occurred when the endpoint was under load – multiple concurrent requests would trigger the thread pool issue.
Once I realized the dependency was the culprit, the fix took 2 minutes: change `def` to `async def` and `yield` to `async yield`. The lesson: when mixing sync and async in FastAPI, the dependency tree must be consistent. A single sync dependency in an async endpoint can cause unpredictable failures.
Root cause
Synchronous generator dependency used in an async endpoint causing FastAPI to execute it in a thread pool, leading to context and cleanup issues.
The fix
Changed the dependency from `def get_db()` to `async def get_db()` with `async yield`.
The lesson
Always match the async nature of your endpoint to its dependencies. If the endpoint is `async def`, every dependency in its chain should be `async def` unless you explicitly intend to use `run_sync`.
FastAPI uses the `Depends` class to mark function parameters as dependencies. When a request comes in, FastAPI inspects the endpoint's signature, finds `Depends` markers, and calls the corresponding callable (the dependency function) before executing the endpoint. The dependency's return value is passed to the endpoint parameter.
If the dependency is a class, FastAPI will instantiate it (calling `__init__` with no arguments) and use that instance. If the class has `__init__` parameters, FastAPI will try to inject those as well, recursively. This is where circular dependencies can occur.
When an endpoint is `async def`, FastAPI expects all dependencies to also be `async def` by default. If a dependency is `def`, FastAPI will attempt to run it synchronously using `anyio.to_thread.run_sync()`. This works for simple functions but fails for generators or context managers because the cleanup (e.g., `finally` block or generator `close()`) runs in a separate thread, causing issues like leaked connections.
To fix, either make the dependency `async def` or set `use_cache=False` and wrap the sync call manually. In practice, the best approach is to keep the async/def consistency across the entire chain.
FastAPI's `app.dependency_overrides` is a powerful testing tool, but it has a pitfall: the override must match the original dependency's signature exactly, including the `async` keyword. If you override a sync dependency with an async function (or vice versa), FastAPI will raise an error at test time.
Another common mistake is using a lambda as an override: `app.dependency_overrides[get_db] = lambda: test_db`. FastAPI will fail because lambdas don't have clear signatures for introspection. Always use a named function or a class with `__call__`.
By default, FastAPI caches the result of a dependency within a single request scope (the same instance is reused if the dependency is called multiple times). This is fine for singletons like database sessions, but can cause bugs if the dependency returns a mutable object that you modify across different parts of the request.
If you need a new instance every time, use `Depends(get_db, use_cache=False)`. But beware: this can lead to multiple database connections per request if not managed carefully.
When a dependency injection error occurs, FastAPI's default error handler returns a 500 with a generic message. To get the full traceback, add a middleware that catches exceptions and logs them: `@app.middleware('http') async def catch_exceptions(request, call_next): try: return await call_next(request) except Exception as e: logger.exception('Unhandled error') raise`. Alternatively, run the app with `--log-level debug` to see the traceback in the console.
Another technique: use `from fastapi.testclient import TestClient` and call `client.get('/endpoint')` – the exception will propagate in the test environment, giving you a full traceback.
Frequently asked questions
Why does FastAPI say 'Depends expects a callable or class'?
You likely passed a non-callable object to Depends, like an instance (e.g., `Depends(my_object)`) or the result of a function call (e.g., `Depends(get_db())`). Depends must receive a callable (function or class) that FastAPI can call to generate the dependency. Check that you are passing the function/class name without parentheses.
Can I use `Depends` with a lambda?
Technically yes, but it's not recommended because FastAPI uses introspection to determine the dependency's signature and to generate OpenAPI schemas. Lambdas have anonymous signatures, which can cause crashes or missing documentation. Use a named inner function or a class instead.
How do I debug a circular dependency?
FastAPI will raise a `RecursionError` or a `RuntimeError` about maximum recursion depth. To fix, refactor your dependencies to avoid cycles. One approach is to use a `@lru_cache` on a factory function that returns the dependency, or split the dependency into smaller pieces. Check the traceback to see which dependencies call each other.
Why does my dependency work in development but fail in production?
This often happens because development uses `uvicorn --reload` which starts a single process, while production uses multiple workers (e.g., `gunicorn -k uvicorn.workers.UvicornWorker`). Multi-process environments can expose race conditions or thread safety issues in dependencies, especially when using sync dependencies in async endpoints. Also, check if you have `use_cache=False` accidentally – in production with multiple workers, caching behavior may differ.
What does the `use_cache` parameter do?
When `use_cache=True` (default), FastAPI caches the result of a dependency within the same request scope. If the same dependency is requested multiple times in one request, the same object is reused. Set `use_cache=False` if you need a fresh instance each time (e.g., per-subrequest database session). However, be careful: disabling cache can increase resource usage.