PAS7 Studio

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.

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

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]

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

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]

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.

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

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]

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

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]

Keep DI singleton

Avoid REQUEST scope

REQUEST-scoped providers can cascade scope through dependencies and add per-request instantiation overhead. Nest explicitly warns about this. [1]

Cross-service propagation

Explicit

ALS only works in-process. Across services, propagate via headers/message metadata. OTel defaults to W3C Trace Context (traceparent). [6][7]

Security

No secrets in context

Don’t propagate sensitive data in context/baggage. OTel explicitly warns against secrets/PII in baggage-like propagated fields. [6]

Diagram: request enters middleware -> ALS store -> services -> logger -> db -> queue propagation

A safe request-context flow: initialize early, read anywhere, propagate explicitly across boundaries

Section pas7-quickstart screenshot

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

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]

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)

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

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]

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

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

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.

FAQ

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]

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.

Related Articles

growthFebruary 15, 2026

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.

Read →
telegram-media-saverJanuary 8, 2025

Automatic Tagging & Search for Saved Links

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

Read →
servicesJanuary 1, 2025

Bot Development & Automation Services

Professional Telegram bot development and business process automation: chatbots, AI assistants, CRM integrations, workflow automation.

Read →
backend-engineeringFebruary 15, 2026

Bun vs Node.js in 2026: Why Bun Feels Faster (and How to Audit Your App Before Migrating)

Bun is shipping a faster, all-in-one JavaScript toolkit: runtime, package manager, bundler, and test runner. Here’s what’s real (with benchmarks), what can break, and how to get a free migration-readiness audit using @pas7-studio/bun-ready.

Read →

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.

Learn more →

Professional development for your business

We create modern web solutions and bots for businesses. Learn how we can help you achieve your goals.