Skip to content
NestJS ns providers 5 min read

Request-Scoped Providers

By default NestJS providers are singletons: one instance is shared across every request for the lifetime of the application. That is fast and memory-efficient, but sometimes you need a fresh instance per request — to capture the authenticated user, a correlation ID, or a tenant identifier without threading that state through every method call. Request-scoped providers solve this by instantiating the provider once per inbound request, and they let you inject the underlying REQUEST object so your services can read per-request context directly.

When you actually need request scope

Most of the time you do not. A singleton service that receives the data it needs as method arguments is simpler and dramatically faster. Reach for Scope.REQUEST only when a provider must hold state tied to a single request and that state is awkward to pass explicitly — for example, a logger that prefixes every line with a request ID, or a repository that must scope queries to the current tenant resolved from a JWT.

ScopeInstancesLifetimeUse when
Scope.DEFAULTOne (singleton)App lifetimeAlmost always
Scope.REQUESTOne per requestSingle requestNeed per-request state/context
Scope.TRANSIENTOne per consumerPer injectionEach consumer needs a private instance

Declaring a request-scoped provider

Set the scope option on @Injectable(). NestJS then creates a new instance of this provider for every incoming request and disposes of it when the request completes.

// request-context.service.ts
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  readonly correlationId: string;
  readonly tenantId: string | undefined;

  constructor(@Inject(REQUEST) private readonly request: Request) {
    this.correlationId =
      (request.headers['x-correlation-id'] as string) ?? crypto.randomUUID();
    this.tenantId = request.headers['x-tenant-id'] as string | undefined;
  }

  describe(): string {
    return `tenant=${this.tenantId ?? 'none'} corr=${this.correlationId}`;
  }
}

The REQUEST token comes from @nestjs/core. With the Express adapter it resolves to the Express Request; under Fastify it resolves to the Fastify FastifyRequest. Type it accordingly.

Injecting and consuming the provider

Any provider that injects a request-scoped provider automatically becomes request-scoped too — scope “bubbles up” the dependency chain. The same is true for the controller that consumes it.

// orders.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { RequestContextService } from './request-context.service';

@Injectable({ scope: Scope.REQUEST })
export class OrdersService {
  constructor(private readonly ctx: RequestContextService) {}

  list(): { context: string; items: string[] } {
    return { context: this.ctx.describe(), items: ['order-1', 'order-2'] };
  }
}
// orders.controller.ts
import { Controller, Get } from '@nestjs/common';
import { OrdersService } from './orders.service';

@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  @Get()
  findAll() {
    return this.orders.list();
  }
}

Output:

$ curl -H "x-tenant-id: acme" -H "x-correlation-id: 8f2a" http://localhost:3000/orders
{"context":"tenant=acme corr=8f2a","items":["order-1","order-2"]}

Scope bubbles upward, never downward. Injecting a request-scoped provider into a singleton makes the singleton request-scoped as well. Be deliberate about where you introduce it, or you may accidentally convert large parts of your graph to per-request instantiation.

Performance implications

Request-scoped providers are not cached. On every request Nest walks the relevant part of the injection container and constructs fresh instances, which adds latency and garbage-collection pressure under load. A controller that is request-scoped is re-instantiated for each call, so any expensive constructor work runs per request.

Practical guidance:

  • Keep the scoped portion of your graph small — isolate per-request state in one thin provider and inject it rather than scoping the whole service tree.
  • Never put request scope on a provider that opens connections or does heavy initialization in its constructor.
  • Singletons can still read request data via durable providers (below) or by accepting it as arguments.

Accessing REQUEST without making the consumer scoped

If a singleton needs request data only occasionally, inject the REQUEST token directly into a narrowly-scoped helper, or use ContextIdFactory / ModuleRef.resolve() to look up a scoped instance on demand instead of converting the consumer.

import { Injectable } from '@nestjs/common';
import { ModuleRef, ContextIdFactory } from '@nestjs/core';

@Injectable()
export class ReportService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async build() {
    const contextId = ContextIdFactory.create();
    const ctx = await this.moduleRef.resolve(RequestContextService, contextId);
    return ctx.describe();
  }
}

Durable providers for multi-tenancy

In multi-tenant systems creating a brand-new instance per request is wasteful when many requests share the same tenant. Durable providers let Nest key scoped instances by a custom value — typically the tenant ID — so instances are reused across requests that resolve to the same key.

Implement a ContextIdStrategy that derives a sub-tree-payload from the request, then mark the provider durable: true.

// tenant.strategy.ts
import {
  HostComponentInfo,
  ContextId,
  ContextIdFactory,
  ContextIdStrategy,
} from '@nestjs/core';
import { Request } from 'express';

const tenants = new Map<string, ContextId>();

export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
  attach(contextId: ContextId, request: Request) {
    const tenantId = (request.headers['x-tenant-id'] as string) ?? 'public';
    let tenantSubTreeId = tenants.get(tenantId);
    if (!tenantSubTreeId) {
      tenantSubTreeId = ContextIdFactory.create();
      tenants.set(tenantId, tenantSubTreeId);
    }
    return (info: HostComponentInfo) =>
      info.isTreeDurable ? tenantSubTreeId! : contextId;
  }
}
// main.ts
import { ContextIdFactory } from '@nestjs/core';
import { AggregateByTenantContextIdStrategy } from './tenant.strategy';

ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());
@Injectable({ scope: Scope.REQUEST, durable: true })
export class TenantConnection {
  /* one instance shared per tenant, not per request */
}

Now TenantConnection is created once per tenant and reused, giving you the isolation of request scope with most of the efficiency of a singleton.

Best Practices

  • Default to singletons; introduce Scope.REQUEST only when per-request state is genuinely required.
  • Keep scoped providers tiny and free of expensive constructor logic to limit per-request cost.
  • Type the injected REQUEST object to your platform (express.Request or Fastify’s FastifyRequest).
  • Remember that scope bubbles upward — auditing one new scoped provider may reveal an unexpectedly large request-scoped subtree.
  • Use durable providers to amortize instantiation in multi-tenant apps where many requests share a key.
  • Prefer ModuleRef.resolve() with a ContextId over converting a singleton when access is occasional.
Last updated June 14, 2026
Was this helpful?