NestJS Request Scopes: Fixing Singleton State Leaks Without Overpaying Per Request
A singleton service that stores the current request ID leaks state across concurrent requests. Use Scope.REQUEST only for genuinely request-bound state, and keep shared services singleton.
30 Oct 2026, 15:45 UTC

The singleton trap
A common pattern in NestJS is to inject a service into a controller, read the current user or request ID from it, and store that value on the service for later use. Because NestJS providers are singletons by default, that stored value is shared across every request the application handles. Under load, two concurrent requests can overwrite each other's data, and one user may see another user's request ID. The useful takeaway: reserve Scope.REQUEST for state that genuinely belongs to a single request, and keep everything else singleton.
Request scope and the REQUEST token
In NestJS, a provider declared with @Injectable() is a singleton: the framework creates one instance and reuses it for every injection point. That is efficient for stateless utilities, but it becomes a bug when the provider holds mutable request data. If a UserContext service stores currentUser on a property, the next request overwrites it. Dependency injection does not isolate the instance per caller; it shares the same object graph.
NestJS lets you change a provider's lifetime with Scope.REQUEST. When you decorate a class with @Injectable({ scope: Scope.REQUEST }), Nest creates a new instance for each incoming request. To reach the underlying HTTP request, you inject the REQUEST token from @nestjs/core. This gives the provider access to headers, parameters, and other request metadata. It is the right tool for request metadata, tenant context, or tracing data that must not be shared.
A minimal request context provider
Here is a focused example. It reads an x-request-id header without storing it on a singleton.
import { Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
constructor(@Inject(REQUEST) private readonly request: Request) {}
get requestId(): string | undefined {
return this.request.headers['x-request-id'] as string | undefined;
}
}
Register it in a module and inject it into a controller:
import { Controller, Get } from '@nestjs/common';
import { RequestContext } from './request-context.service';
@Controller()
export class AppController {
constructor(private readonly ctx: RequestContext) {}
@Get()
find() {
return { requestId: this.ctx.requestId };
}
}
A request with x-request-id: abc returns abc. A concurrent request with x-request-id: xyz returns xyz. The provider holds no shared mutable state.
The hidden cost of scope bubbling
Request scope is not free. If you inject a request-scoped provider into a singleton service, NestJS bubbles the request scope up through that dependency chain. The consumer becomes request-scoped too, even if you did not decorate it. A small change—adding one constructor parameter—can silently increase per-request object creation and alter testing assumptions. Singletons that depend on request-scoped state are often a design smell; pass the value as a method argument instead.
Verifying the behavior
To confirm the fix works, add an end-to-end test that sends two concurrent GET requests with different x-request-id headers and assert that each response contains its own ID. If you suspect accidental bubbling, temporarily log a provider instance identifier or constructor call and make two requests; request-scoped instances should appear separately while singletons remain shared. Run these tests against the actual HTTP adapter, because GraphQL, microservices, and other transports may expose different request objects or lifecycle semantics.
Trade-offs and limitations
Request scope is not a replacement for authorization, validation, or tenant isolation. Those checks must still enforce the correct subject and data boundaries. Avoid storing large or expensive resources in request-scoped state; repeated construction can turn a convenience into a measurable per-request cost. Exact imports, request types, and lifecycle behavior are version- and adapter-sensitive, so confirm them against your installed NestJS major and platform.
An actionable default
Start by passing request-specific values as method arguments. If that becomes unwieldy, introduce a small request-context provider with Scope.REQUEST. Keep shared, stateless collaborators singleton. When in doubt, check whether a singleton is holding mutable request data—if it is, that is the leak.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.