Skip to content
NestJS ns providers 5 min read

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).

ScopeInstances createdShared across requestsTypical use
Scope.DEFAULTOne, at bootstrapYesAlmost everything
Scope.REQUESTOne per inbound requestNoPer-request state (tenant, user, trace id)
Scope.TRANSIENTOne per consumerNoLightweight, 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 REQUEST token (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.
Last updated June 14, 2026
Was this helpful?