Managing Per-Request State in NestJS: When to Move Beyond Singletons
Stop passing user and tenant IDs through every service method. Learn how to use NestJS Request Scope to handle per-request state cleanly while understanding the performance trade-offs.
02 Sept 2026, 08:36 UTC

The Problem: Passing User Context Through Every Layer
In a typical NestJS application, providers are Singletons by default. This is efficient, but it creates a architectural headache when you need data that changes with every single HTTP request—such as a tenant ID for a multi-tenant database, a correlation ID for logging, or the authenticated user's permissions.
The common (but tedious) solution is "parameter drilling": passing the user or tenantId object as an argument through every single service method from the Controller down to the Repository. This litters your business logic with plumbing code that has nothing to do with the actual feature.
The Solution: Request-Scoped Providers
NestJS provides Scope.REQUEST to solve this. When a provider is marked as request-scoped, the DI (Dependency Injection) container creates a new instance of that provider for every incoming request. This allows you to inject the REQUEST object directly into your service, giving you access to headers, query params, and user data without passing them manually through every function call.
Implementing a Request-Scoped Context Service
To implement this, you need a provider that captures the request data and makes it available to other services. This is particularly useful for multi-tenancy where the database connection depends on a header.
// 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 TenantContextService {
private tenantId: string;
constructor(@Inject(REQUEST) private request: Request) {
// Extract tenant ID from a custom header
this.tenantId = request.headers['x-tenant-id'] as string;
}
getTenantId(): string {
return this.tenantId;
}
}Now, any service that injects TenantContextService can access the current tenant without the Controller having to pass it explicitly:
// order.service.ts
@Injectable()
export class OrderService {
constructor(private readonly tenantContext: TenantContextService) {}
async findOrders() {
const tenantId = this.tenantContext.getTenantId();
return this.orderRepository.find({ where: { tenantId } });
}
}The "Bubble Effect" and Dependency Chains
A critical behavior of the NestJS DI container is the Scope Bubble. If a Singleton provider (the default) injects a Request-scoped provider, the Singleton provider automatically becomes Request-scoped.
- Singleton: One instance for the app lifetime.
- Request: New instance per request.
- Transient: New instance every time it is injected.
If OrderService is a Singleton but injects TenantContextService (Request), OrderService is now instantiated for every request. This chain continues upward to the Controller. While this makes state management easy, it changes how your application consumes memory.
Performance Trade-offs and Limitations
Request scoping is not a "free" architectural win. There are two primary risks to consider:
- Garbage Collection Overhead: In high-throughput applications (thousands of requests per second), creating and destroying a tree of providers for every single call increases memory pressure and triggers more frequent GC cycles.
- Bootstrapping Complexity: Because these providers are created at runtime, you cannot easily use them in certain global guards or interceptors that are instantiated during the application bootstrap phase.
Verifying Scope Behavior
To verify if your services are actually being recreated, add a simple unique identifier to your provider's constructor:
@Injectable({ scope: Scope.REQUEST })
export class TenantContextService {
private readonly id = Math.random();
constructor() {
console.log(`Provider created with ID: ${this.id}`);
}
}Run the application and make three separate HTTP requests. If you see three different IDs in the console, the request scope is working. If you see the same ID three times, the provider is behaving as a Singleton.
Summary Decision Matrix
| Use Case | Recommended Scope | Reasoning |
|---|---|---|
| Stateless Utility / DB Connection Pool | Singleton | Maximum performance, shared state. |
| Multi-tenant Context / User Session | Request | Avoids parameter drilling for request-specific data. |
| Unique Logger per Component | Transient | Ensures each class has its own isolated instance. |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.