Managing Per-Request State in NestJS with Request-Scoped Providers
Learn how to use Scope.REQUEST in NestJS to handle per-request state like tenant IDs and auth contexts without leaking data between users.
17 Oct 2025, 01:13 UTC

The Problem: Leaking State Between Users
In a standard NestJS application, providers are singletons by default. This means a single instance of a service is shared across every single incoming request. While this is efficient, it creates a critical problem when you need to store data that is unique to a specific user or request—such as a tenant ID in a multi-tenant app, a correlation ID for logging, or a user's locale.
If you store a tenantId as a class property in a singleton service, Request A might set it to "Client_1", but before Request A finishes, Request B might overwrite it to "Client_2". Request A then continues processing using Client B's data, leading to catastrophic data leaks.
The Solution: Scope.REQUEST
NestJS solves this using Request-scoped providers. By changing the scope of a provider, you instruct the NestJS IoC (Inversion of Control) container to create a fresh instance of that service for every incoming request. Once the request is completed and the response is sent, the instance is garbage collected.
To implement this, you use the Scope.REQUEST enum within the @Injectable() decorator. This ensures that the service is isolated to the current execution context.
The Bubble Effect
A critical architectural detail of request scoping is the "bubble effect." If Service A is request-scoped, and Service B injects Service A, then Service B automatically becomes request-scoped, regardless of its own defined scope. This propagation continues up the dependency tree to the Controller.
This behavior ensures that no singleton ever accidentally holds a reference to a request-specific instance, which would violate the isolation guarantee.
Worked Example: Multi-Tenant Context
Imagine a scenario where you need to extract a x-tenant-id header from every request and make it available to your database services without passing it as a function argument through every single layer of your app.
// tenant-context.service.ts
import { Injectable, Scope, Inject } from '@nestjs';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({
scope: Scope.REQUEST,
})
export class TenantContext {
private tenantId: string;
constructor(@Inject(REQUEST) private request: Request) {
// Extract the tenant ID from the request headers
this.tenantId = request.headers['x-tenant-id'] as string;
}
getTenantId(): string {
return this.tenantId;
}
}
Now, any service that injects TenantContext will have access to the correct ID for that specific request:
// users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly tenantContext: TenantContext) {}
findAll() {
const tenantId = this.tenantContext.getTenantId();
return `Fetching users for tenant: ${tenantId}`;
}
}
Verification Steps
- Run the application and send two concurrent requests with different
x-tenant-idheaders. - Add a
console.log(this)in theTenantContextconstructor. - Expected Result: You should see the constructor log trigger for every single request, and the
tenantIdshould match the header of that specific request.
Performance Trade-offs and Limitations
Request scoping is a powerful tool, but it is not free. You should be aware of the following constraints:
- Instantiation Overhead: Because NestJS must recreate the provider and all its dependents for every request, there is a measurable increase in CPU and memory usage compared to singletons. In high-throughput systems, this can lead to increased latency.
- Singleton Incompatibility: You cannot inject a request-scoped provider into a singleton provider. If you try to do this, NestJS will throw a dependency resolution error at startup because a singleton cannot depend on something that changes every request.
- Memory Pressure: Under extreme concurrency, the rapid creation and disposal of these objects can increase the frequency of Garbage Collection (GC) cycles.
Decision Matrix: Singleton vs. Request Scope
| Feature | Scope.DEFAULT (Singleton) | Scope.REQUEST |
|---|---|---|
| Lifecycle | App Startup to Shutdown | Request Start to Response |
| Performance | High (Instantiated once) | Lower (Instantiated per request) |
| State | Shared across all users | Isolated per user/request |
| Best Use Case | Configuration, DB Connection Pools | Auth Context, Multi-tenancy, Logging IDs |
Closing Action
When deciding whether to use Scope.REQUEST, first ask if the data can be passed as a method argument. If the data is needed across many layers (e.g., Controller → Service → Repository), request scoping is the cleanest architectural choice. To minimize the performance hit, keep your request-scoped tree shallow—only make the specific services that require the request state scoped, rather than applying it to your entire application.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.