Managing the 'Bubble-Up' Effect in NestJS Request-Scoped Providers
Learn how NestJS Request-scoped providers work, why the 'bubble-up' effect can kill your application performance, and how to verify your DI container's behavior.
16 Jul 2025, 02:00 UTC

The Hidden Cost of Request-Specific Data
You need to access the current user's identity or a specific correlation ID across multiple services in your NestJS application. The most intuitive approach is to create a provider with Scope.REQUEST, allowing you to inject the REQUEST object directly into your service. However, you soon notice that your application's memory usage is climbing and response times are lagging.
The problem is the bubble-up effect. In NestJS, dependency injection is hierarchical. If a Singleton provider (the default) depends on a Request-scoped provider, that Singleton is automatically converted to Request-scoped. This creates a chain reaction: every service that depends on that Singleton also becomes Request-scoped, forcing the DI container to rebuild a significant portion of your provider tree for every single incoming HTTP request.
Understanding Provider Scopes
To manage this, you must first distinguish between the three primary scopes available in the @Injectable() decorator:
- DEFAULT (Singleton): A single instance is created on application startup and shared globally. This is the most performant option.
- REQUEST: A new instance is created for every incoming request and garbage collected once the response is sent.
- TRANSIENT: A new instance is created for every provider that injects it, regardless of the request cycle.
Implementing a Request-Scoped Provider
When you truly need request-specific data—such as a tenant ID for a multi-tenant database—you can implement a scoped provider. This requires importing the REQUEST token from @nestjs/core.
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({
scope: Scope.REQUEST,
})
export class TenantService {
private readonly tenantId: string;
constructor(@Inject(REQUEST) private request: Request) {
// Extract tenant ID from headers
this.tenantId = request.headers['x-tenant-id'] as string;
console.log(`TenantService instantiated for tenant: ${this.tenantId}`);
}
getTenantId(): string {
return this.tenantId;
}
}
The Bubble-Up Example
Consider this dependency chain: TenantService (Request) → OrderService (Singleton) → OrderController. Because OrderService injects TenantService, OrderService is no longer a Singleton. It is now instantiated for every request, and consequently, the OrderController is also instantiated per request.
Trade-offs and Performance Risks
While Request scoping provides a clean way to handle per-request state, it introduces two primary risks:
- Memory Overhead: In high-throughput applications, the constant allocation and deallocation of provider instances increase garbage collection pressure.
- Latency: The DI container must resolve the entire dependency graph for every call, which adds milliseconds to the request pipeline.
To avoid the bubble-up effect, consider using the Module Ref approach or passing the required data as a method argument rather than injecting the entire request object into the service constructor.
Verifying the Scope
To check if a provider has accidentally become Request-scoped, you can use a simple logging check. Add a console.log('Provider Initialized') to the constructor of your suspected Singleton. If you see this log every time you refresh your browser or hit an API endpoint, the provider has bubbled up to Request scope.
Rollback Strategy: If you find that Request scoping is degrading performance, revert the scope: Scope.REQUEST property to Scope.DEFAULT. You will then need to refactor your services to accept request-specific data (like userId) as parameters in their methods rather than relying on constructor injection.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.