PAS7 Studio
Back to all articles

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.

08 Feb 2026· 13 min read· Technology
NestJS request context blueprint: request-scoped DI vs AsyncLocalStorage (ALS), logging and tracing

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]

- You use AsyncLocalStorage-based context (clean call-sites, keeps providers singleton, but needs correct setup and awareness of edge cases). [2][3]

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]

TS
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

PackageCore approachBest forKey trade-offs
nestjs-clsALS + ClsService + Proxy Providers + pluginsfull-featured context + transactionsmore abstraction; learn its API surface
@pas7/nestjs-request-contextALS + typed keys + decorators + adaptersstrict, lightweight, typed request contextnewer ecosystem; depends on your needed integrations
@medibloc/nestjs-request-contextALS-based request contextsimple ALS-based contextnarrower scope vs nestjs-cls
nestjs-pino (context part)logging integration using ALScontextual logging with Pinologging-focused (not a general context framework)
DIY (Nest recipe)raw ALSminimal + full controlyou 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 screenshot

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)

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

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

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

- traceparent (W3C Trace Context) if you have distributed tracing. [6][7]

HTTP outbound example (pseudo-pattern)

TS
const headers = {
  'x-request-id': RequestContextService.get(REQUEST_ID),
  'traceparent': currentTraceparent,
};

Queue job example (pseudo-pattern)

TS
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]

- Queue propagation tests: publish → consume restores context explicitly. [6][7]

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-cls if you need a mature ecosystem (transactions, proxy providers, broader use cases). [8]

  • Choose @pas7/nestjs-request-context if 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

Is REQUEST scope in NestJS always bad?

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]

Is AsyncLocalStorage “officially supported” in NestJS?

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]

Can ALS context magically cross microservices or queues?

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]

What can cause ALS context loss?

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]

Is `@pas7/nestjs-request-context` relevant and competitive for this topic?

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]

Do I need to worry about Node.js security updates if I use ALS?

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.

0

We publish practical, citeable articles on web engineering, automation, security, and product development — focused on decisions that actually matter in production.

Related Articles

growth

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.

blogs

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.

telegram-media-saver

Automatic Tagging & Search for Saved Links

Integrate with GDrive/S3/Notion for automatic tagging and fast search via search APIs

services

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.