Injection Scopes
Every provider in NestJS has a lifetime, and by default that lifetime is the entire application. A single instance is created at bootstrap and shared across every request that ever touches it. This singleton behaviour is fast and memory-efficient, but it is not always what you want — sometimes a provider must carry state that is unique to a single request, or you genuinely need a fresh instance at every injection point. Injection scopes give you precise control over exactly when and how often the container instantiates a provider.
The three scopes
Nest defines three scopes through the Scope enum exported from @nestjs/common. You opt into a non-default scope by passing it to the @Injectable() decorator (or to a custom provider definition).
| Scope | Instances created | Shared across requests | Typical use |
|---|---|---|---|
Scope.DEFAULT | One, at bootstrap | Yes | Almost everything |
Scope.REQUEST | One per inbound request | No | Per-request state (tenant, user, trace id) |
Scope.TRANSIENT | One per consumer | No | Lightweight, stateful helpers |
Default (singleton) scope
When you write a plain @Injectable(), the provider is a singleton. The container builds it once and hands the same reference to everyone. There is no need to specify Scope.DEFAULT explicitly — it is the implicit behaviour.
// metrics/metrics.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class MetricsService {
private counter = 0;
increment(): number {
return ++this.counter;
}
}
Because there is exactly one MetricsService, the counter is shared process-wide — perfect for caches, connection pools, and stateless business logic.
Request scope
A request-scoped provider gets a brand-new instance for every inbound request, and that instance is destroyed once the request completes. This lets you inject the current request and store per-request data without leaking it between users.
// context/tenant.service.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class TenantService {
readonly tenantId: string;
constructor(@Inject(REQUEST) private readonly request: Request) {
this.tenantId = (request.headers['x-tenant-id'] as string) ?? 'public';
}
}
The REQUEST token resolves to the underlying platform request object (the Express Request above, or the Fastify request when using @nestjs/platform-fastify).
Transient scope
A transient provider is never shared. Each consumer that injects it receives its own dedicated instance. Two different services depending on the same transient provider get two different objects.
// logging/scoped-logger.service.ts
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class ScopedLogger {
private context = 'default';
setContext(name: string): void {
this.context = name;
}
log(message: string): void {
console.log(`[${this.context}] ${message}`);
}
}
Each service can call setContext() on its private copy without affecting any other consumer.
Scope bubbles up the chain
This is the rule that catches people out: scope is contagious upward. If a singleton depends on a request-scoped provider, the singleton can no longer be a singleton — it must be re-created per request too, so it is promoted to request scope. The promotion propagates all the way up the injection chain, including to the controller.
Consider a controller that injects a service, which in turn injects a request-scoped TenantService:
// catalog/catalog.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CatalogService } from './catalog.service';
@Controller('catalog')
export class CatalogController {
constructor(private readonly catalog: CatalogService) {}
@Get()
list() {
return this.catalog.listForTenant();
}
}
// catalog/catalog.service.ts
import { Injectable } from '@nestjs/common';
import { TenantService } from '../context/tenant.service';
@Injectable() // looks like a singleton...
export class CatalogService {
constructor(private readonly tenant: TenantService) {}
listForTenant() {
return { tenant: this.tenant.tenantId, items: [] };
}
}
Even though CatalogService and CatalogController declare no explicit scope, both are instantiated per request because they transitively depend on a REQUEST-scoped provider.
Output:
GET /catalog (tenant header: acme) -> new CatalogController, new CatalogService, new TenantService
GET /catalog (tenant header: globex) -> new CatalogController, new CatalogService, new TenantService
Scope promotion is silent. A provider you wrote as a singleton can quietly become request-scoped just because something deep in its dependency tree is request-scoped — with all the performance implications that brings.
The performance cost of request scope
Singletons are created once; request-scoped providers are created on every request. That means extra allocation, garbage collection, and a per-request walk of the dependency sub-graph that touches the scoped provider. Under high throughput this is measurable. Keep request scope confined to the smallest set of providers that genuinely need per-request state, and avoid letting it bubble into hot, high-frequency services unless you have to.
Durable providers
Multi-tenant applications often want per-tenant instances rather than per-request instances — reused across many requests that share the same tenant. Durable providers solve this by combining request scope with a ContextIdStrategy that groups requests by a stable key, so Nest caches one sub-tree per tenant instead of rebuilding it every request.
// context/durable.service.ts
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantStore {
private readonly data = new Map<string, unknown>();
set(key: string, value: unknown) {
this.data.set(key, value);
}
get(key: string) {
return this.data.get(key);
}
}
// context/tenant.strategy.ts
import { ContextId, ContextIdFactory, ContextIdStrategy, HostComponentInfo } from '@nestjs/core';
import { Request } from 'express';
const tenants = new Map<string, ContextId>();
export class TenantContextStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = (request.headers['x-tenant-id'] as string) ?? 'public';
let tenantSubId = tenants.get(tenantId);
if (!tenantSubId) {
tenantSubId = ContextIdFactory.create();
tenants.set(tenantId, tenantSubId);
}
return (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubId! : contextId;
}
}
Register the strategy once during bootstrap with ContextIdFactory.apply(new TenantContextStrategy()) and every durable provider is now scoped per tenant, dramatically reducing instantiation churn.
Best Practices
- Default to
Scope.DEFAULT. Reach for request or transient scope only when shared singleton state is actually a problem. - Inject the
REQUESTtoken (from@nestjs/core) only inside request-scoped providers — it is meaningless in a singleton. - Be aware of scope bubbling: a single request-scoped dependency can promote an entire controller chain, so audit transitive dependencies.
- Keep request-scoped providers thin and shallow to minimise the per-request construction cost.
- Use durable providers for multi-tenant scenarios so instances are reused per tenant instead of per request.
- Prefer transient scope for stateful helpers (like contextual loggers) where each consumer needs its own configured copy.