Skip to main content

Troubleshooting Dependency Scopes

FastAPI uses dependency scopes to manage the lifecycle of dependencies, particularly those that use yield (generator dependencies). When these lifecycles conflict—specifically when a dependency that lives longer depends on one that is cleaned up sooner—FastAPI raises a DependencyScopeError.

Understanding Dependency Scopes

Dependencies in FastAPI can have one of two scopes:

  1. "function": The dependency is resolved and, if it is a generator, cleaned up as soon as the specific dependency that requested it is finished.
  2. "request": The dependency lives for the entire duration of the HTTP request. If it is a generator, it is only cleaned up after the response has been sent to the client.

By default, regular functions that use return do not have a strict scope enforcement because they have no teardown logic. However, generator dependencies (using yield) default to the "request" scope. This ensures that resources like database sessions remain open even if a StreamingResponse is returned, as the generator's teardown code (the code after yield) only runs after the request is fully finished.

The DependencyScopeError

The DependencyScopeError (found in fastapi.exceptions) is a safety mechanism enforced during application startup. It prevents a "wider" scope dependency from depending on a "narrower" one.

Specifically, a dependency with "request" scope cannot depend on a dependency with "function" scope.

If this were allowed, the "request" scoped dependency might try to use a sub-dependency that has already been closed and cleaned up by the "function" scope logic.

Triggering the Error

This error typically occurs when you have a generator dependency (which defaults to "request") that depends on another dependency explicitly marked as "function" scope.

In tests/test_dependency_yield_scope.py, a broken configuration is demonstrated:

from typing import Annotated, Any
from fastapi import Depends, FastAPI
from fastapi.exceptions import DependencyScopeError

app = FastAPI()

def dep_session() -> Any:
s = Session()
yield s
s.close()

# Explicitly marked as "function" scope
SessionFuncDep = Annotated[Any, Depends(dep_session, scope="function")]

def get_named_session(session: Annotated[Any, SessionFuncDep]) -> Any:
# This generator defaults to "request" scope
yield {"session": session, "name": "named"}

@app.get("/broken-scope")
def get_broken(sessions: Annotated[Any, Depends(get_named_session)]):
return sessions

In this example:

  1. get_named_session is a generator, so its computed_scope is "request".
  2. It depends on SessionFuncDep, which is explicitly scoped to "function".
  3. FastAPI detects that get_named_session (living until the end of the request) depends on SessionFuncDep (which would be closed as soon as get_named_session finishes its setup), and raises DependencyScopeError.

Implementation Details

The validation logic resides in fastapi/dependencies/utils.py within the get_dependant function. During the recursive analysis of the dependency tree, FastAPI checks the following condition:

# fastapi/dependencies/utils.py

if (
(dependant.is_gen_callable or dependant.is_async_gen_callable)
and dependant.computed_scope == "request"
and param_details.depends.scope == "function"
):
raise DependencyScopeError(
f'The dependency "{call_name}" has a scope of '
'"request", it cannot depend on dependencies with scope "function".'
)

The computed_scope is determined in the Dependant class (fastapi/dependencies/models.py):

@property
def computed_scope(self) -> str | None:
if self.scope:
return self.scope
if self.is_gen_callable or self.is_async_gen_callable:
return "request"
return None

Resolving the Conflict

To resolve a DependencyScopeError, you must ensure the parent dependency does not outlive its children.

Option 1: Promote the Sub-dependency to Request Scope

If the sub-dependency should stay open for the whole request, remove the explicit scope="function" or change it to scope="request".

# Change the sub-dependency to "request" scope (or remove the scope parameter)
SessionRequestDep = Annotated[Any, Depends(dep_session, scope="request")]

def get_named_session(session: Annotated[Any, SessionRequestDep]) -> Any:
yield {"session": session, "name": "named"}

Option 2: Demote the Parent to Function Scope

If the parent dependency does not need to stay open until the very end of the request (e.g., it doesn't support a StreamingResponse), you can explicitly set its scope to "function".

def get_named_session(session: Annotated[Any, SessionFuncDep]) -> Any:
yield {"session": session, "name": "named"}

# Explicitly mark the parent as "function" scope
@app.get("/fixed-scope")
def get_fixed(sessions: Annotated[Any, Depends(get_named_session, scope="function")]):
return sessions

By aligning the scopes, you ensure that the AsyncExitStack (or ExitStack) managing the dependencies cleans them up in the correct order without leaving any dependency holding a reference to a closed resource.