Technology
The NestJS Request Context Problem: Request-Scoped DI vs AsyncLocalStorage (ALS) — a Practical Production Blueprint (2026)
A deep, source-backed guide to the most painful NestJS problem: request context (user, tenant, correlation IDs, tracing) without turning your DI graph into REQUEST scope. Includes comparisons, real-world patterns, and a practical blueprint featuring @pas7/nestjs-request-context.

What you’ll get from this article
This is not a “theory post”. It’s a practical guide you can apply in a real NestJS codebase — with verified sources and a clear decision framework.
• What “request context” actually means in NestJS (and why it’s painful). [1][2][3]
• Why REQUEST-scoped providers feel easy — but often become a performance and architecture trap. [1]
• How AsyncLocalStorage (ALS) solves the core pain (and where it can still fail). [2][3]
• How companies typically do it: correlation IDs, tracing, OpenTelemetry propagation, safe context boundaries. [4][5][6][7]
• A curated comparison of the most relevant packages (with pros/cons and “best for”). [8][9][10][11][12]
• A production blueprint + code patterns you can copy (HTTP + queues + microservices). [2][6][7]
• Where @pas7/nestjs-request-context fits, and when it’s a competitive choice. [10]
The most painful NestJS problem: request context without collateral damage
“Request context” is everything you want to access deep in services without threading it through every method: current user, tenant, permissions, request ID, trace IDs, transaction handle, locale, feature flags, and more.
In NestJS, you usually end up with one of these outcomes:
- You pass context explicitly as parameters everywhere (reliable, but noisy and hard to maintain).
- You use REQUEST-scoped providers (simple API, but it can turn a big chunk of your app into per-request instantiation). [1]
Why REQUEST-scoped DI becomes a trap (and why teams regret it later)
NestJS clearly warns about it: request-scoped providers add overhead because instances are created per request, and — more importantly — scope “spreads” through dependencies. [1]
Two quotes that matter when you’re designing architecture:
> “Any provider that relies on a request-scoped provider automatically adopts a request scope, and this behavior cannot be altered.” [1]
> “Using request-scoped providers will have an impact on application performance... it will still have to create an instance of your class on each request.” [1]
This is how real apps get into trouble: a single “request-scoped datasource / logger / context” becomes a root dependency, and suddenly controllers + services + repositories are recreated per request. NestJS even calls out multi-tenant apps as a common scenario where this happens. [1]
Yes, Nest mentions the impact might be “~5% latency-wise” in a properly designed app — but the real cost is usually architectural: it’s harder to reason about lifetime, caching, and the “shape” of your DI tree. [1]
AsyncLocalStorage in one minute: why it maps perfectly to request context
Node.js AsyncLocalStorage is essentially “thread-local storage for async code”: it lets you associate state with an execution chain (promises/callbacks) and read it later without passing parameters. [3]
Node’s docs recommend AsyncLocalStorage as the preferred approach over rolling your own async_hooks-based implementation, describing it as performant and memory-safe. [3]
But it’s not magic. Context loss can still happen in “rare situations”, especially around callback-based APIs or custom thenables — Node recommends promisifying or using AsyncResource to bind execution context correctly. [3]
So the question becomes: can we reliably wrap the Nest request lifecycle so everything downstream sees the same store? NestJS answers: yes — via middleware / lifecycle entry point. [2]
NestJS official direction: ALS as an alternative to REQUEST scope
NestJS explicitly positions ALS as a way to propagate state without passing parameters and as an alternative to REQUEST-scoped providers “and some of their limitations”. [2]
Core idea: wrap the request early (middleware) with als.run(store, () => next()). Then any provider can read from ALS store later.
Minimal NestJS-style example (simplified from the official recipe): [2]
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { AsyncLocalStorage } from 'node:async_hooks';
const als = new AsyncLocalStorage<{ requestId: string; userId?: string }>();
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req: any, _res: any, next: () => void) => {
const store = {
requestId: String(req.headers['x-request-id'] ?? crypto.randomUUID()),
userId: req.headers['x-user-id'] ? String(req.headers['x-user-id']) : undefined,
};
als.run(store, () => next());
})
.forRoutes('*path');
}
}This is the “clean core” pattern. Real production setups add: safe header parsing, response header echo, trace correlation, queue propagation, and testing.
How companies actually solve it: correlation IDs + tracing + safe propagation
In production systems, request context is not only about “current user”. The most common drivers are observability and incident response: you need logs, traces, and metrics to correlate reliably across services.
That’s exactly what OpenTelemetry context propagation is about: moving trace/span context across process and network boundaries (defaulting to W3C Trace Context headers like traceparent). [6][7]
Two practical takeaways from enterprise reality:
- You can keep “in-process” context in ALS, but across service boundaries you must propagate it explicitly (HTTP headers, message metadata, etc.). [6][7]
- Don’t put sensitive data into cross-service context propagation. OpenTelemetry explicitly warns against putting secrets/PII into baggage-like propagated key-values. [6]
A 2026 operational note you should not ignore if you rely on ALS heavily (directly or via APM/tracing): Node.js shipped a mitigation for an async_hooks/ALS-related denial-of-service edge case in January 2026 and recommends updating patched versions. Major APM and OTel teams published mitigation guidance and context around it. [4][5]
Package landscape (2026): what exists, what’s solid, what’s risky
Below is a high-signal comparison of packages that real teams use to solve NestJS request context. The goal is not “who has more stars”, but: correctness, ergonomics, integrations, and how well it avoids REQUEST scope.
Quick comparison table
| Package | Core approach | Best for | Key trade-offs |
|---|---|---|---|
nestjs-cls | ALS + ClsService + Proxy Providers + plugins | full-featured context + transactions | more abstraction; learn its API surface |
@pas7/nestjs-request-context | ALS + typed keys + decorators + adapters | strict, lightweight, typed request context | newer ecosystem; depends on your needed integrations |
@medibloc/nestjs-request-context | ALS-based request context | simple ALS-based context | narrower scope vs nestjs-cls |
nestjs-pino (context part) | logging integration using ALS | contextual logging with Pino | logging-focused (not a general context framework) |
| DIY (Nest recipe) | raw ALS | minimal + full control | you own edge cases + testing |
Now the details with sources:
- nestjs-cls positions itself as a continuation-local storage module for NestJS built on AsyncLocalStorage, listing use cases like request ID tracking, multi-tenancy, and propagating transactions without explicit parameter passing. [8]
- nestjs-pino documents a key argument many maintainers repeat: avoiding REQUEST-scoped providers due to performance drops, using AsyncLocalStorage for request-bound loggers instead. [11]
- Nest’s official recipe acknowledges ALS as a viable alternative to REQUEST scope — but also states NestJS does not provide a built-in abstraction itself, meaning you either implement it or use a library. [2]
Deep dive: when `nestjs-cls` is the best choice
If your context needs are broad (multi-tenancy + transactions + proxy providers + “request context where REQUEST scope is not supported”), nestjs-cls is often the most complete toolbox. Its docs emphasize a wide range of use cases and explicitly mentions avoiding clunky REQUEST-scoped approaches. [8]
A frequent reason teams pick it: they want transaction propagation across services without passing a transaction object everywhere, plus a supported ecosystem of plugins around this pattern. [8]
If you’re already deeply invested in that ecosystem, switching away later is usually not worth it — but if you only need 2–3 fields (requestId, userId, tenantId) you may want something smaller.
Where `@pas7/nestjs-request-context` fits (and when it’s competitive)
@pas7/nestjs-request-context is directly aligned with this problem: providing request context via AsyncLocalStorage while keeping your DI tree singleton, with a strong focus on type-safety via typed keys and clean ergonomics for NestJS. [10]
What makes it particularly relevant to this article:
- Typed ContextKey<T> for context values (less stringly-typed “magic keys”). [10]
- NestJS-oriented ergonomics: decorators (e.g. parameter decorators) so you can read context without plumbing. [10]
- HTTP adapters for Express and Fastify in NestJS environments. [10]
- It explicitly positions itself against the REQUEST-scope approach from a performance/architecture standpoint. [10]
Caveat (honesty check): the repo notes that using the Fastify adapter outside NestJS is limited due to Fastify + AsyncLocalStorage incompatibilities, so you should treat it as NestJS-first. [10]
Practical setup: a production-friendly blueprint (copy/paste patterns)
This section is the “do it right” checklist: where to initialize context, what to store, how to propagate to queues/microservices, and how to test it so you don’t ship context leaks.
Recommended context fields (practical default)
- requestId: stable correlation ID; echo back as x-request-id in response.
- userId: internal user identifier (avoid email/phone).
- tenantId: if multi-tenant.
- traceId / spanId: if you run tracing (or derive from OTel context).
- authLevel / role: if you need it for authorization decisions (prefer explicit checks in critical flows).
Pattern A: Minimal DIY ALS (good if you want full control)
Use the Nest recipe’s middleware approach and implement a small wrapper (with tests). [2]
Pattern B: Use nestjs-cls when you need the ecosystem
If you need transaction propagation plugins and proxy provider patterns, nestjs-cls is a strong choice. [8]
Pattern C: Use @pas7/nestjs-request-context when you want strict typed keys + lean surface area
If your need is a clean, typed request context (requestId/userId/tenantId + a few more), and you value a NestJS-first adapter layer, PAS7’s library is a sensible fit. [10]
Initialization point
Initialize context as early as possible (middleware is the safest for HTTP in Nest). Nest’s recipe uses middleware specifically for this reason. [2]
Keep DI singleton
REQUEST-scoped providers can cascade scope through dependencies and add per-request instantiation overhead. Nest explicitly warns about this. [1]
Cross-service propagation
ALS only works in-process. Across services, propagate via headers/message metadata. OTel defaults to W3C Trace Context (traceparent). [6][7]
Security
Don’t propagate sensitive data in context/baggage. OTel explicitly warns against secrets/PII in baggage-like propagated fields. [6]

A safe request-context flow: initialize early, read anywhere, propagate explicitly across boundaries
Section pas7-quickstart screenshotCode patterns: `@pas7/nestjs-request-context` (typed keys + ergonomic access)
Below are illustrative patterns based on the library’s approach (typed ContextKey<T>, Nest-first ergonomics, adapters). Always verify exact API names against the repo before locking in production usage. [10]
1) Define typed keys (one module/shared package)
import { ContextKey } from '@pas7/nestjs-request-context';
export const REQUEST_ID = new ContextKey<string>('requestId');
export const USER_ID = new ContextKey<string | undefined>('userId');
export const TENANT_ID = new ContextKey<string | undefined>('tenantId');2) Initialize in middleware (HTTP entry point)
import { Injectable, NestMiddleware } from '@nestjs/common';
import type { Request, Response } from 'express';
import { RequestContextService } from '@pas7/nestjs-request-context';
import { REQUEST_ID, USER_ID, TENANT_ID } from './ctx.keys';
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
const requestId = String(req.headers['x-request-id'] ?? crypto.randomUUID());
const userId = req.headers['x-user-id'] ? String(req.headers['x-user-id']) : undefined;
const tenantId = req.headers['x-tenant-id'] ? String(req.headers['x-tenant-id']) : undefined;
res.setHeader('x-request-id', requestId);
RequestContextService.run(() => {
RequestContextService.set(REQUEST_ID, requestId);
RequestContextService.set(USER_ID, userId);
RequestContextService.set(TENANT_ID, tenantId);
next();
});
}
}3) Read context in services (no param threading)
import { Injectable } from '@nestjs/common';
import { RequestContextService } from '@pas7/nestjs-request-context';
import { REQUEST_ID, USER_ID } from './ctx.keys';
@Injectable()
export class BillingService {
charge() {
const requestId = RequestContextService.get(REQUEST_ID);
const userId = RequestContextService.get(USER_ID);
return { requestId, userId };
}
}The core win: your services stay singleton and clean, while still accessing per-request context. [10]
Queues & microservices: the boundary where many implementations break
ALS does not magically cross process boundaries. If you publish a job to a queue, or call another service, you must propagate context explicitly (like requestId/trace context). [6][7]
A good default is to propagate:
- x-request-id (your correlation ID)
HTTP outbound example (pseudo-pattern)
const headers = {
'x-request-id': RequestContextService.get(REQUEST_ID),
'traceparent': currentTraceparent,
};Queue job example (pseudo-pattern)
await queue.add('jobName', {
data: payload,
ctx: {
requestId: RequestContextService.get(REQUEST_ID),
tenantId: RequestContextService.get(TENANT_ID),
},
});When consuming the job, initialize a fresh ALS context and restore the propagated fields before executing business logic.
If you choose request-scoped consumers for jobs, Nest notes that they create a new instance per job — similar trade-offs to HTTP request scope. [13]
Testing: how to avoid silent context leaks in production
Context bugs are notorious because they look fine locally but break under concurrency.
Your minimum testing strategy should include:
- Parallel request tests: two simultaneous requests must not see each other’s context.
- “Async boundary” tests: delayed tasks (setTimeout, message handlers) must retain context (or intentionally not retain it, if you design it that way). [3]
PAS7’s library includes a testing story in-repo (and positions itself with a testkit approach); if you go DIY, you must build equivalent guardrails. [10]
What you should choose (a simple decision rule)
If you want a fast decision without rereading everything:
• Choose DIY ALS if you want minimal dependencies and you’re ready to own testing and edge cases. Start from Nest’s recipe. [2]
• Choose
nestjs-clsif you need a mature ecosystem (transactions, proxy providers, broader use cases). [8]• Choose
@pas7/nestjs-request-contextif you want a lean, type-safe, NestJS-first context layer with adapters and clean ergonomics. [10]• Avoid REQUEST-scoped DI as a default — use it only when you truly need per-request instantiation and can control the blast radius. Nest warns about performance and cascading scope. [1]
If you’re building a serious NestJS product
Request context is one of those “small” architectural decisions that determines whether your codebase stays clean at 30 endpoints or collapses at 300.
If you’re building a NestJS product and want help with backend architecture, observability, or automation — PAS7 Studio can help design and implement production-grade systems.
Read more: https://pas7.com.ua/blog
Sources and cross-references
All links below are directly relevant to the topic and were used to ground the factual claims and comparisons above.
• 1. NestJS docs — Injection scopes (REQUEST scope behavior, cascading scope, performance notes) Read source ↗
• 2. NestJS docs — Async Local Storage recipe (official ALS approach and rationale) Read source ↗
• 3. Node.js docs — AsyncLocalStorage & troubleshooting context loss (official behavior and recommendations) Read source ↗
• 4. Node.js blog — DoS mitigation advisory related to async_hooks/AsyncLocalStorage ecosystem reliance (Jan 2026) Read source ↗
• 5. OpenTelemetry — JS statement on Node.js DoS mitigation (Jan 2026) Read source ↗
• 6. OpenTelemetry docs — Context propagation concepts, security notes, and W3C Trace Context default Read source ↗
• 7. W3C — Trace Context specification (traceparent header format standard) Read source ↗
• 8. nestjs-cls — official documentation (use cases and ALS-based approach) Read source ↗
• 10. PAS7 Studio — @pas7/nestjs-request-context repository (typed keys, adapters, ergonomics, limitations) Read source ↗
• 11. nestjs-pino — repository (ALS-based contextual logging argument vs REQUEST scope) Read source ↗
• 12. Medibloc — nestjs-request-context repository (ALS-based request context library) Read source ↗
• 13. NestJS docs — Queues (request-scoped consumers instantiation per job) Read source ↗
• 14. Datadog — mitigation guidance referencing ALS/APM impact (Jan 2026) Read source ↗
FAQ
No — but it’s risky as a default. Nest warns that request-scoped providers impact performance and can cascade scope through dependencies. Use it only when you truly need per-request instantiation. [1]
NestJS provides an official recipe and explains how ALS can serve as an alternative to REQUEST-scoped providers, but it does not ship a built-in abstraction layer. [2]
No. ALS is in-process only. Across service boundaries you must propagate context explicitly (headers/message metadata). OpenTelemetry defaults to W3C Trace Context (`traceparent`) for tracing. [6][7]
Node notes context loss can happen in rare situations, especially around callback-based APIs or custom thenables; recommended fixes include promisifying or using AsyncResource to bind context correctly. [3]
Yes — it targets exactly this pain: typed request context over ALS with NestJS-first ergonomics and adapters. It’s a strong fit when you want a lean, type-safe layer without pushing your app into REQUEST scope. [10]
You should keep Node patched. In January 2026, Node shipped a mitigation related to async_hooks/ALS ecosystem reliance, and OTel/APM vendors published upgrade guidance. [4][5][14]
Want more deep, source-backed engineering breakdowns?
We publish practical, citeable articles on web engineering, automation, security, and product development — focused on decisions that actually matter in production.