FastAPI’s Depends() system is one of its best features — until it silently fails or throws a cryptic 500 error at runtime. The code looks right, startup succeeds, and the first real request blows up.

Quick Answer: Most FastAPI dependency errors fall into four categories: mixing sync/async functions incorrectly, database sessions not cleaning up from yield dependencies, circular imports between router files, or misunderstanding how FastAPI caches dependencies per request. Check these four areas first.

Diagnostic Steps

Before diving into specific causes, run this quick checklist when a dependency breaks:

  1. Read the full traceback — is it a RuntimeError, AttributeError, or a database error? The exception type narrows it down fast.
  2. Test the dependency in isolation — call it directly in a script to see if it works outside FastAPI’s DI container.
  3. Check if the error is intermittent — flaky errors usually point to session management or async boundary issues.
  4. Temporarily add print() statements at the start and end of each dependency — FastAPI’s DI is eager, so you’ll quickly see which one never completes.

Use Debugly’s trace formatter to parse Python tracebacks from nested dependency chains. When the error originates three levels deep in SQLAlchemy or an auth middleware, the formatted output makes the root cause obvious.


Cause #1: Mixing Sync and Async Dependencies

This trips up almost every developer coming from Flask or Django. FastAPI supports both sync and async dependencies, but calling one from the other in the wrong direction causes either a hard crash or silent event loop blocking.

The most common mistake: calling asyncio.run() inside a sync dependency to “unwrap” an async function.

# This code triggers the error:
import asyncio
from fastapi import Depends, FastAPI

app = FastAPI()

async def fetch_user_from_db(user_id: int):
    await asyncio.sleep(0.01)
    return {"id": user_id, "name": "Alice"}

def get_current_user(user_id: int = 1):
    # WRONG: asyncio.run() creates a new event loop,
    # but FastAPI already has one running.
    user = asyncio.run(fetch_user_from_db(user_id))
    return user

@app.get("/profile")
async def profile(user=Depends(get_current_user)):
    return user
RuntimeError: This event loop is already running.

asyncio.run() tries to create and run a new event loop, but there’s already one running inside FastAPI/Uvicorn. The fix is simple — make the dependency async if it needs to call async functions:

# Option 1: Make the dependency async
async def get_current_user(user_id: int = 1):
    user = await fetch_user_from_db(user_id)
    return user

@app.get("/profile")
async def profile(user=Depends(get_current_user)):
    return user

# Option 2: Keep everything sync (use a sync DB driver)
def fetch_user_sync(user_id: int):
    # psycopg2, not asyncpg
    return {"id": user_id, "name": "Alice"}

def get_current_user_sync(user_id: int = 1):
    return fetch_user_sync(user_id)

@app.get("/profile")
async def profile(user=Depends(get_current_user_sync)):
    return user

FastAPI automatically runs sync dependencies in a thread pool, so a sync dependency that calls sync database code works fine. You don’t need async everywhere — just don’t mix the two at the boundary. For a deeper look at how async/sync boundaries affect your entire application, see our guide on fastapi async sync blocking errors.


Cause #2: Database Sessions Not Cleaning Up

FastAPI’s yield-based dependencies are the standard way to manage database sessions. The code after yield acts as cleanup — it always runs, even if the route handler raises an exception. When this cleanup is incomplete, you get connection pool exhaustion, stale transactions, or data that never commits.

Here’s the naive version that developers write first:

# Incomplete — no error handling:
from sqlalchemy.orm import Session

def get_db():
    db = SessionLocal()
    yield db
    db.close()  # runs on cleanup, but no rollback on error!

If your route raises an exception mid-transaction, db.close() still runs — but you’ve left a partial transaction uncommitted. On some database drivers, this hangs until it times out, slowly draining your connection pool. The production-ready version:

# Production-ready yield dependency:
def get_db():
    db = SessionLocal()
    try:
        yield db
        db.commit()
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()

The try/except/finally guarantees three things: successful requests commit automatically, failed requests roll back (no partial writes), and the session always closes regardless of what happened.

A second, related mistake is using a sync Session in an async route:

# Wrong: sync session in async route blocks the event loop
@app.get("/items")
async def get_items(db: Session = Depends(get_db)):
    items = db.query(Item).all()  # This blocks!
    return items

# Right: async session for async routes
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

async def get_async_db():
    async with AsyncSessionLocal() as db:
        yield db

@app.get("/items")
async def get_items(db: AsyncSession = Depends(get_async_db)):
    result = await db.execute(select(Item))
    return result.scalars().all()

Connection pool exhaustion from session leaks is a particularly nasty production issue — see our deep dive on SQLAlchemy pool exhaustion in FastAPI for more on diagnosing it under load.


Cause #3: Circular Imports Between Router Files

As your app grows and you split routes into multiple files, circular imports become a real hazard. FastAPI doesn’t catch these at startup — you’ll see an ImportError or AttributeError when the first request hits an affected route.

Typical scenario: auth.py imports a helper from users.py, and users.py imports the auth dependency from auth.py.

# The import chain that breaks:
# main.py → auth.py → users.py → auth.py  ← circular!

# auth.py
from app.routers.users import get_user_by_email  # imports from users

# users.py
from app.routers.auth import get_current_user    # imports from auth
ImportError: cannot import name 'get_user_by_email' from partially
initialized module 'app.routers.users'

Fix 1: Extract shared dependencies to a dedicated module

This is the cleanest long-term solution. Create a dependencies.py file that nothing else imports from:

# app/dependencies.py — imports nothing from your routers
from sqlalchemy.ext.asyncio import AsyncSession

async def get_db():
    async with AsyncSessionLocal() as db:
        try:
            yield db
            await db.commit()
        except Exception:
            await db.rollback()
            raise

# app/routers/auth.py — only imports from dependencies.py
from app.dependencies import get_db

async def get_current_user(db: AsyncSession = Depends(get_db)):
    ...

# app/routers/users.py — imports from dependencies.py AND auth.py (one direction)
from app.dependencies import get_db
from app.routers.auth import get_current_user

@router.get("/me")
async def get_me(
    user=Depends(get_current_user),
    db: AsyncSession = Depends(get_db)
):
    ...

The rule: dependencies flow in one direction. auth depends on dependencies, users depends on both dependencies and auth. Neither goes in reverse.

Fix 2: Lazy imports inside the dependency function

If refactoring is too risky right now, delay the import until the function is called:

async def get_current_user():
    # Import happens at call time, not at module load time
    from app.routers.users import get_user_by_email
    user = await get_user_by_email(...)
    return user

This breaks the circular reference at module load time. It’s not elegant, but it’s a safe quick fix while you plan the refactor.


Cause #4: Misunderstanding Dependency Caching

FastAPI caches dependencies by default within a single request. If two parameters in the same request both declare Depends(get_db), they receive the same session object — not two separate ones. This is usually correct behavior, but it trips up developers in two ways.

Scenario A: you expect two independent sessions but get one:

@app.get("/example")
async def example(
    db1: AsyncSession = Depends(get_db),
    db2: AsyncSession = Depends(get_db),
):
    print(db1 is db2)  # True! FastAPI returned the same cached instance.

Most of the time this is what you want — opening two connections per request is wasteful. But if you genuinely need independent sessions (rare in application code, more common in testing), use use_cache=False:

@app.get("/example")
async def example(
    session1: AsyncSession = Depends(get_db, use_cache=False),
    session2: AsyncSession = Depends(get_db, use_cache=False),
):
    print(session1 is session2)  # False — distinct instances

Scenario B: you expect cached data to persist between requests, but it doesn’t:

Dependencies are cached within a request, not across requests. Every new HTTP request gets fresh dependency instances. If you’re seeing stale data, the problem is in your database session configuration (like autoflush=False without an explicit flush), not in FastAPI’s caching.

For state that should survive across requests, use FastAPI’s application state or a proper caching layer:

from contextlib import asynccontextmanager
import redis.asyncio as aioredis

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: initialize shared, long-lived resources
    app.state.redis = await aioredis.from_url("redis://localhost")
    yield
    # Shutdown: cleanup
    await app.state.redis.close()

app = FastAPI(lifespan=lifespan)

# Access in a dependency:
from fastapi import Request

def get_redis(request: Request):
    return request.app.state.redis

@app.get("/cached")
async def get_cached(redis=Depends(get_redis)):
    return await redis.get("my_key")

Cause #5: Sub-Dependencies Failing Silently

FastAPI resolves the entire dependency tree before calling your route handler. If any dependency in the chain raises an unhandled exception, FastAPI returns a generic 500 with minimal detail. This makes it hard to diagnose which dependency actually failed, especially in a chain of three or four.

# A dependency that fails with no useful context:
async def get_redis_client():
    client = await aioredis.from_url("redis://localhost")
    return client
    # If Redis is down: ConnectionRefusedError becomes a plain 500

@app.get("/cached-data")
async def get_data(redis=Depends(get_redis_client)):
    return await redis.get("key")

Fix: Raise HTTPException with context inside the dependency

from fastapi import HTTPException
import aioredis

async def get_redis_client():
    try:
        client = await aioredis.from_url(
            "redis://localhost",
            socket_connect_timeout=2
        )
        await client.ping()  # Verify the connection is alive
        return client
    except (aioredis.ConnectionError, ConnectionRefusedError):
        raise HTTPException(
            status_code=503,
            detail="Cache service unavailable. Try again later."
        )

Now API consumers get a meaningful 503 instead of a generic 500, and your monitoring can distinguish infrastructure failures from application bugs.

For complex chains, add structured logging at each dependency level:

import logging
logger = logging.getLogger(__name__)

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    logger.debug("Resolving user from token prefix: %s", token[:10])
    try:
        user = await verify_token_and_fetch_user(token, db)
        logger.debug("Resolved user id=%s", user.id)
        return user
    except InvalidTokenError as e:
        logger.warning("Token validation failed: %s", e)
        raise HTTPException(status_code=401, detail="Invalid token")

When the 500 hits, your logs will show exactly which dependency logged last — that’s your failure point.


Still Not Working?

A few less-common issues worth checking:

Dependency never runs: FastAPI only resolves dependencies declared in the function signature with Depends(). Defining a dependency but not wiring it up means it never executes.

# Wrong: dependency defined but not included as a parameter
async def log_request():
    print("Request received")

@app.get("/items")
async def get_items():   # log_request never runs!
    return []

# Right: wire it up, use underscore for unused return values
@app.get("/items")
async def get_items(_=Depends(log_request)):
    return []

Class-based dependencies creating new instances per request: if your dependency is a callable class with expensive setup (like establishing a connection), FastAPI creates a new instance on every request unless you cache it.

# New instance per request — expensive:
class SearchClient:
    def __init__(self):
        self.client = build_elasticsearch_client()  # slow!

    def __call__(self):
        return self

# Better: module-level singleton
_search_client = None

def get_search_client():
    global _search_client
    if _search_client is None:
        _search_client = SearchClient()
    return _search_client

Or initialize it once in the lifespan handler and access it via app.state, as shown in the caching section above.

Dependencies in background tasks: BackgroundTasks run after the response is sent. If you pass a database session from a dependency into a background task, the session may be closed by the time the task runs (since the dependency cleanup fires when the request completes). Always create a fresh session inside background tasks:

from fastapi import BackgroundTasks

async def send_welcome_email_task(user_id: int):
    # Create a NEW session here — don't reuse the request session
    async with AsyncSessionLocal() as db:
        user = await db.get(User, user_id)
        await send_email(user.email, "Welcome!")

@app.post("/register")
async def register(
    user_data: UserCreate,
    background_tasks: BackgroundTasks,
    db: AsyncSession = Depends(get_db)
):
    user = await create_user(db, user_data)
    background_tasks.add_task(send_welcome_email_task, user.id)
    return user

Summary Checklist

When a FastAPI dependency breaks, work through this list:

  • [ ] Async/sync mismatch — does any sync dependency call asyncio.run() inside a running loop? Make it async instead.
  • [ ] Yield cleanup — do all yield dependencies have try/except/finally blocks so cleanup runs on errors too?
  • [ ] Circular imports — do any two router files import from each other? Move shared deps to a standalone dependencies.py.
  • [ ] Caching behavior — are you expecting fresh instances but getting cached ones (or vice versa)? Check use_cache.
  • [ ] Silent failures — do dependencies that call external services raise HTTPException on failure, not raw exceptions?
  • [ ] Dependency wired up — is it actually declared as a parameter with Depends() in the route function?
  • [ ] Background task sessions — are you reusing a request-scoped session inside a background task? Create a fresh one.

Use Debugly’s trace formatter to analyze tracebacks from nested dependency chains — when the error originates deep in an auth middleware or ORM layer, Debugly’s formatted output highlights the exact line that caused the failure without you having to parse dozens of frames manually.

FastAPI’s dependency injection system is powerful precisely because it handles complex wiring automatically — but that automation means errors are sometimes non-obvious. Once you’ve seen each of these failure modes once, they’re immediately recognizable. Keep this checklist handy, and you’ll go from “why is this 500ing?” to a fix in minutes.