Skip to main content

Caching and Lifecycle Scopes

FastAPI manages the execution of dependencies through a sophisticated system of caching and lifecycle scopes. This ensures that resources are shared efficiently within a single request while providing granular control over when those resources are initialized and torn down.

The Dependant Blueprint

At the heart of the dependency system is the Dependant class (found in fastapi/dependencies/models.py). This class acts as a metadata container for a dependency, storing everything needed to resolve it: the callable to execute (call), its own sub-dependencies (dependencies), and configuration flags like use_cache.

When a route is registered, FastAPI recursively builds a tree of Dependant objects. This tree is then used during the request-response cycle by solve_dependencies to compute the actual values.

Dependency Caching

Caching in FastAPI is designed to prevent the redundant execution of the same dependency within a single request. If multiple dependencies or the path operation itself require the same dependency, FastAPI will, by default, execute it once and reuse the result.

The Cache Key

The uniqueness of a dependency for caching purposes is determined by the cache_key property in the Dependant class:

@cached_property
def cache_key(self) -> DependencyCacheKey:
scopes_for_cache = (
tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
)
return (
self.call,
scopes_for_cache,
self.computed_scope or "",
)

The cache key is a combination of:

  1. The callable itself.
  2. The security scopes (if any) required by the dependency.
  3. The computed scope of the dependency.

This means that if the same function is used with different security requirements, it will be treated as a distinct dependency and executed separately.

Controlling Cache Behavior

Developers can opt-out of caching by setting use_cache=False in the Depends() declaration. This is useful for dependencies that must perform an action every time they are referenced, such as a counter or a logging utility.

In fastapi/dependencies/utils.py, the solve_dependencies function checks this flag before looking into the dependency_cache:

if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key]

If use_cache is False, FastAPI bypasses the cache lookup and executes the dependency again, even if it has already been resolved in the current request.

Lifecycle Scopes

FastAPI supports two primary lifecycle scopes: function and request. These scopes determine the lifetime of a dependency, particularly those using yield (generators).

Function vs. Request Scope

  • function scope: The dependency is resolved and, if it's a generator, its cleanup code (after the yield) is executed immediately after the endpoint function returns.
  • request scope: The dependency's cleanup is deferred until the entire response has been sent to the client. This is the default for generator dependencies.

The computed_scope property in Dependant determines the default behavior:

@cached_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

Impact on Streaming Responses

The distinction between these scopes is critical when using StreamingResponse. A dependency with function scope will be closed before the stream even starts, which would cause errors if the stream relies on a resource (like a database session) provided by that dependency. Conversely, a request scope dependency remains open throughout the streaming process.

Constraints and Tradeoffs

Scope Mismatch

A significant constraint in the implementation is that a dependency with a broader scope cannot depend on one with a narrower scope. Specifically, a request scope dependency cannot depend on a function scope dependency. This is because the request scope dependency might outlive the function scope one, leading to use-after-free scenarios for resources.

This is enforced in fastapi/dependencies/utils.py within the get_dependant function:

if (
scope == "request"
and sub_dependant.scope == "function"
):
raise DependencyScopeError(
f"Dependency {call!r} with scope 'request' cannot depend on "
f"dependency {sub_dependant.call!r} with scope 'function'."
)

State Management via SolvedDependency

The results of the resolution process are encapsulated in the SolvedDependency class. This class holds the dependency_cache for the duration of the request resolution:

class SolvedDependency:
values: dict[str, Any]
errors: list[Any]
background_tasks: StarletteBackgroundTasks | None
response: Response
dependency_cache: dict[DependencyCacheKey, Any]

By passing this dependency_cache through recursive calls to solve_dependencies, FastAPI ensures that the "single request" context is maintained across the entire dependency tree. This design choice prioritizes efficiency and consistency, ensuring that all parts of a request see the same instance of a shared resource.