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.

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]
“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]
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]
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 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.
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]
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]
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.
@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]
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]
Early lifecycle
Initialize context as early as possible (middleware is the safest for HTTP in Nest). Nest’s recipe uses middleware specifically for this reason. [2]
Avoid REQUEST scope
REQUEST-scoped providers can cascade scope through dependencies and add per-request instantiation overhead. Nest explicitly warns about this. [1]
Explicit
ALS only works in-process. Across services, propagate via headers/message metadata. OTel defaults to W3C Trace Context (traceparent). [6][7]
No secrets in context
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 screenshotBelow 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]
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]
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]
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]
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: /blog
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]
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)
• 2. NestJS docs — Async Local Storage recipe (official ALS approach and rationale)
• 5. OpenTelemetry — JS statement on Node.js DoS mitigation (Jan 2026)
• 6. OpenTelemetry docs — Context propagation concepts, security notes, and W3C Trace Context default
• 7. W3C — Trace Context specification (traceparent header format standard)
• 8. nestjs-cls — official documentation (use cases and ALS-based approach)
• 11. nestjs-pino — repository (ALS-based contextual logging argument vs REQUEST scope)
• 12. Medibloc — nestjs-request-context repository (ALS-based request context library)
• 13. NestJS docs — Queues (request-scoped consumers instantiation per job)
• 14. Datadog — mitigation guidance referencing ALS/APM impact (Jan 2026)
We publish practical, citeable articles on web engineering, automation, security, and product development — focused on decisions that actually matter in production.
Related Articles
AI SEO / GEO in 2026: Your Next Customers Aren’t Humans — They’re Agents
Search is shifting from clicks to answers. Bots and AI agents crawl, cite, recommend, and increasingly buy. Learn what AI SEO / GEO means, why classic SEO is no longer enough, and how PAS7 Studio helps brands win visibility in the agentic web.
The most powerful Apple chip yet? M5 Pro and M5 Max are breaking records
A data-backed March 2026 analysis of Apple M5 Pro and M5 Max. We break down why these chips can credibly be called Apple's most powerful pro laptop silicon, how they compare with M4 Pro, M4 Max, M1 Pro, M1 Max, and how they stack up against Intel and AMD laptop rivals.
Automatic Tagging & Search for Saved Links
Integrate with GDrive/S3/Notion for automatic tagging and fast search via search APIs
Bot Development & Automation Services
Professional Telegram bot development and business process automation: chatbots, AI assistants, CRM integrations, workflow automation.
Web Development for Your Business
Professional development of modern web applications and websites
Turnkey Website Development
Website development services for business: landing pages, corporate websites, and ecommerce builds with integrations, fast performance, and SEO-ready architecture.
Professional development for your business
We create modern web solutions and bots for businesses. Learn how we can help you achieve your goals.