NestJS Provider Scopes and the Traps That Catch You in Production

2026-09-137 min readTwixr Solutions

Cover image for NestJS Provider Scopes and the Traps That Catch You in Production

NestJS Provider Scopes and the Traps That Catch You in Production

Your NestJS app leaks request data between users. You added scope: Scope.REQUEST to one provider and now your throughput dropped by 40%. You have no idea which provider is causing it.

All three problems trace back to the same misunderstanding: provider scopes.

Here is the practical checklist. Read it once before you ship, not after.


The 3 Scopes - Plain English

NestJS gives you three lifecycle options for any injectable provider.

  • DEFAULT (Singleton) - one instance for the lifetime of the application. Shared by every request.
  • REQUEST - a fresh instance per incoming HTTP request. Destroyed after the response.
  • TRANSIENT - a fresh instance every time a consumer injects it. Multiple consumers in the same request each get their own copy.

The default is DEFAULT. You opt in to anything else.

![Diagram showing the three NestJS provider scopes - Singleton, Request, and Transient - with instance lifecycles per request][https://res.cloudinary.com/fprznalq/image/upload/v1789182402/twixr-studio/ptrjgldm5zxxzd0ijeqf.jpg]


The Checklist

1. Default to Singleton - Always

If you are unsure, use singleton. It is zero overhead. The instance is created once, wired once, and reused forever.

Most services belong here: repositories, config readers, HTTP clients, queue producers, logger wrappers.

Ask yourself: "Does this service hold any state that is unique to one request?" If the answer is no, it is a singleton.

2. Only Use REQUEST Scope When You Actually Need Per-Request State

The legitimate uses are narrow:

  • You need cls-hooked or AsyncLocalStorage-style request context without external libraries
  • You are injecting REQUEST (the raw Express/Fastify request object) into a service and need it available deep in the call tree
  • You are building a per-request audit trail that writes to a database row tied to that request

Do not use REQUEST scope just to feel safer about shared state. Fix the shared-state bug instead.

3. Understand Scope Bubbling - This Is Where Apps Break

This is the most important rule on this list.

If a singleton injects a REQUEST-scoped provider, NestJS silently promotes the singleton to REQUEST scope.

That promotion bubbles up the entire dependency tree. A single scope: Scope.REQUEST on a low-level utility can turn your top-level service - and every provider that touches it - into a REQUEST-scoped provider.

Result: thousands of instances created and destroyed per second under load. Memory spikes. CPU overhead from garbage collection. Response times climb.

Check the tree before you set any non-singleton scope. Map out who injects what.

4. Never Inject the Raw REQUEST Object Into a Singleton

// WRONG - this silently upgrades your singleton to REQUEST scope
@Injectable()
export class UserService {
  constructor(@Inject(REQUEST) private readonly req: Request) {}
}

If you need request metadata inside a singleton (tenant ID, user ID, correlation ID), use AsyncLocalStorage or a purpose-built context module like nestjs-cls. Inject context, not the request.

5. TRANSIENT Scope Is Rarely the Right Answer

TRANSIENT means a new instance per injection point. It is not "a new instance per request" - it is more granular than that. Two consumers in the same module each get separate instances.

The only real use case: a provider that holds genuinely isolated mutable state per consumer (think a builder pattern object). Everything else is overkill.

If you are reaching for TRANSIENT because you think REQUEST is too heavy, the real answer is usually to refactor the state out of the provider entirely.

6. Durable Providers for Long-Lived Non-Singleton State

NestJS 8+ added durable providers - a way to reuse instances across requests that share the same "context" (e.g., the same tenant in a multi-tenant app). The pattern: define a custom context identifier, mark the provider as durable, and NestJS manages a pool of instances keyed by that context.

This is the right tool when you find yourself wanting one instance per tenant rather than one per request. It avoids the full overhead of REQUEST scope while still giving you isolation where it counts.

7. Debugging Scope Issues in Practice

When something looks wrong, these are the places to look:

  • Memory grows linearly with traffic - a singleton was silently promoted to REQUEST scope, and something in the chain holds a reference
  • Intermittent data bleed between requests - a singleton is mutating shared state; the scope is not the bug, the mutable singleton state is
  • Slow startup - TRANSIENT or REQUEST-scoped providers being eagerly instantiated; check for onModuleInit side effects in non-singleton providers
  • DI errors about circular dependencies - often surface when scope promotion creates unexpected dependency graphs

The NestFactory.create() call logs the module graph in debug mode. Use it:

const app = await NestFactory.create(AppModule, { logger: ['debug'] });

Pair that with NEST_DEBUG=true to trace every provider instantiation. Noisy, but worth it when you are chasing a scope bug.

![Code card comparing correct AsyncLocalStorage-based context injection versus incorrect raw REQUEST object injection in a NestJS singleton service][https://res.cloudinary.com/fprznalq/image/upload/v1789182413/twixr-studio/ltai7wcbgueqfvx1c2mi.jpg]

8. Testing Gotchas

Unit tests hide scope bugs because the testing module creates providers once and reuses them. The bug only surfaces under concurrent load.

Write an integration test that fires 50 parallel requests and asserts per-request isolation. Jest's built-in concurrency is enough. Do not wait for production to find this.

9. Document Non-Default Scopes in Code

Every scope: Scope.REQUEST or scope: Scope.TRANSIENT should have a comment explaining why. Not for future-you - for the next engineer who reads the provider and wonders why it is not a singleton.

@Injectable({
  // REQUEST scope: injects tenant-specific DB connection resolved per request.
  // Switching to singleton would share a single tenant connection across all requests.
  scope: Scope.REQUEST,
})
export class TenantDatabaseService {}

One comment. Saves 30 minutes of debugging later.


Quick Reference Table

ScopeInstance PerTypical Use CaseOverhead
DEFAULTApplicationMost services, repositories, clientsNone
REQUESTHTTP RequestPer-request context, audit rowsMedium
TRANSIENTInjection PointBuilder patterns, isolated mutable stateHigh
DurableCustom Context KeyMulti-tenant instance poolsLow-Medium

The Pattern That Fixes Most Problems

Stop storing request state in providers. Push it to AsyncLocalStorage (Node 16+, no external dependency). Your providers stay singletons. Your request context travels transparently through the call stack. No scope bubbling. No memory overhead.

The nestjs-cls package wraps this in an idiomatic NestJS interface if you want a ready-made solution.

If you are building AI-driven request pipelines where context needs to flow through multiple services - something I covered in the work behind AI Automation & Chatbots - this pattern is essential. A REQUEST-scoped provider per LLM call is a fast path to a memory problem.


This kind of subtle dependency injection issue is not unique to NestJS. If you work across Laravel too, the N+1 problem is a similar class of "looks fine locally, breaks under load" trap - worth reading Overusing with() and Missing the Real N+1 in Laravel if that stack is also in your toolbox.

![Diagram of NestJS scope bubbling - a REQUEST-scoped provider promoting its singleton consumer up the dependency tree, with an annotation showing where the promotion starts][https://res.cloudinary.com/fprznalq/image/upload/v1789182421/twixr-studio/b8ixmpzjbwn9vfjha2q1.jpg]


The Short Version

  • Singleton unless you have a specific reason.
  • REQUEST scope bubbles up. Map the tree first.
  • Never inject the raw request into a singleton.
  • Use AsyncLocalStorage for context. Keep providers stateless.
  • Document every non-default scope with a one-line reason.

Frequently asked questions

DEFAULT (singleton). One instance is created when the application bootstraps and shared across every request for the lifetime of the process. You have to explicitly opt in to REQUEST or TRANSIENT scope.

Enjoyed this article?

Get notified when I publish new posts on SaaS, Laravel, and remote engineering.

Keep reading

More posts

2026-09-164 min read

Laravel 12, NestJS 10, React 19: The One Feature Worth Caring About in Each

Three major releases landed. Most of the changelog is noise. Here is the one feature in Laravel 12, NestJS 10, and React 19 that actually changes how you ship.

Read

2026-08-277 min read

Model Observers vs Events vs Listeners in Laravel: Pick the Right Tool

Model observers, events, and listeners all hook into your app's lifecycle - but using the wrong one creates tangled, untestable code. Here's how to choose.

Read

2026-09-187 min read

Partial and Composite Indexes: The Ordering Rule That Kills Slow Queries

Get composite and partial index ordering wrong and your query planner ignores the index entirely. Here is the rule, with concrete examples.

Read