FastAPI uses an explicit dependency injection system built on Python type hints. It's a great feature until your application scales and shared state starts leaking between requests. You define dependencies using standard functions, and the framework resolves them automatically when a route runs. The core issue lies in how the framework caches instances and how long those instances stay alive in memory. It's not immediately obvious to developers coming from other frameworks.
By default, FastAPI caches dependencies per-request when using Depends. If you declare a dependency multiple times within a single request chain, the framework evaluates it once and reuses that exact instance. This behavior changes entirely if you instantiate stateful objects outside the request lifecycle, or if you use classes that retain internal attributes across distinct client connections. A single misplaced object initialization turns a stateless web server into a shared-state nightmare where user data crosses boundaries.
Understanding Request Versus Singleton Lifecycles
You need to know where your objects live. When you write a class and instantiate it inside a module-level variable or pass an instance directly into a dependency function without using the framework's caching mechanisms, you create a singleton. Every incoming request hits that exact same object instance. If that object stores user data, configuration changes, or database connections in its own instance attributes, you've got a severe production bug on your hands.
Consider a counter service or a client cache. If you attach state to the dependency class itself rather than request-scoped parameters, the values persist. The next request from a different user reads the previous user's data. This behavior catches developers off guard, especially those transitioning from synchronous frameworks where request isolation is handled differently. For an overview of how FastAPI compares to alternative stacks, you can read Django vs FastAPI vs Flask: Pick the Right Python Stack. It's essential to grasp these architectural differences early.
Here's how a state leak manifests when using a class instance directly as a default parameter instead of letting FastAPI manage the lifecycle through dependency injection:
from fastapi import FastAPI, Depends
class RequestCounter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
counter = RequestCounter()
app = FastAPI()
@app.get("/unsafe")
def unsafe_endpoint():
return {"count": counter.increment()}
In the snippet above, counter is a module-level singleton. Every call to /unsafe increments the exact same integer across all concurrent worker threads or async tasks. If you want proper isolation, you must let FastAPI instantiate the class on every request. Don't rely on global state for per-user operations.
Properly Scoping Dependencies With Classes
To fix the shared state issue, you must wrap your dependencies so that the framework controls their creation and destruction. When you pass the class itself to Depends() instead of an instance, FastAPI instantiates that class fresh for every incoming request. When the request finishes, the object goes out of scope and gets garbage collected, preventing any data leaks between users. It's a clean separation that protects your application boundaries.
You also need to handle cleanup tasks correctly. If your dependency opens network connections or database sessions, you should use Python generators with yield. This pattern allows FastAPI to run cleanup code after the response leaves the server. If you struggle with dangling network resources or unclosed connections, check out Fixing Async Python Memory Leaks from Unclosed Sessions for concrete solutions. You'll avoid resource exhaustion in production.
Here's the corrected implementation using proper dependency injection scoping where a fresh instance is created for each request:
from fastapi import FastAPI, Depends
class RequestCounter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
app = FastAPI()
def get_counter() -> RequestCounter:
return RequestCounter()
@app.get("/safe")
def safe_endpoint(counter: RequestCounter = Depends(get_counter)):
return {"count": counter.increment()}
By using a factory function or passing the class directly to Depends(RequestCounter), FastAPI ensures the state is isolated. The counter resets for every request because a new class instance is generated on demand. It's a reliable pattern that scales.
Avoiding Stateful Sub-Dependencies
Scope leaks frequently hide in sub-dependencies. A top-level route might look clean, but it calls a service dependency that holds a reference to a global configuration object or a mutable cache dictionary. If you mutate that cache during request processing, every subsequent request sees the mutation. You must ensure that every layer of your dependency tree respects request boundaries. Don't let shared mutable state creep into your service layer.
When building larger applications, keep your database sessions, user contexts, and external API clients bound strictly to the generator yield lifecycle. If a dependency needs to be shared across multiple endpoints within the exact same request, FastAPI handles that automatically via its internal request-scoped cache. Do not implement custom global singletons to avoid instantiation overhead unless you're dealing with stateless utility functions or read-only configuration loaders that never change after startup.













