AsyncLocalStorage (ALS) is the right default for request IDs and tenant context — until a 50k RPS gateway pays for a store enter/exit on every hop through middleware, ORM, and fetch. Measure the overhead on your Node version, then choose ALS, explicit params, or a hybrid.
⚡ TL;DR: Benchmark ALS vs explicit
ctxargs on hot handlers; keep one ALS store for correlation; don’t nest stores per library; disable ALS in ultra-hot pure functions. Pair with Node Diagnostic Channels and Node.js Event Loop Lag p99.
Benchmark before policy
// scripts/bench-als.ts
import { AsyncLocalStorage } from "node:async_hooks";
import { performance } from "node:perf_hooks";
const als = new AsyncLocalStorage<{ reqId: string }>();
function bare(n: number) {
let x = 0;
for (let i = 0; i < n; i++) x += i;
return x;
}
function withAls(n: number) {
return als.run({ reqId: "b" }, () => {
let x = 0;
for (let i = 0; i < n; i++) {
als.getStore(); // typical middleware read
x += i;
}
return x;
});
}
function bench(fn: (n: number) => number) {
const n = 5_000_000;
const t0 = performance.now();
fn(n);
return performance.now() - t0;
}
console.log({ bare: bench(bare), als: bench(withAls) });
Run on the same Node major as production. If ALS adds single-digit percent on a CPU-bound microbench, real I/O-bound APIs often absorb it — but gateways that already fight for microseconds should stay skeptical.
Lean context pattern
// lib/context.ts
import { AsyncLocalStorage } from "node:async_hooks";
export type ReqCtx = { reqId: string; tenantId: string };
export const reqAls = new AsyncLocalStorage<ReqCtx>();
export function getCtx(): ReqCtx {
const s = reqAls.getStore();
if (!s) throw new Error("missing_request_context");
return s;
}
// ✅ Hot pure math / codec path — pass args, skip ALS
export function encodeInvoice(totalCents: number, tenantId: string) {
return Buffer.from(`${tenantId}:${totalCents}`);
}
// server middleware — one enter per request
app.use((req, res, next) => {
reqAls.run(
{ reqId: req.headers["x-request-id"] as string ?? crypto.randomUUID(),
tenantId: String(req.headers["x-tenant-id"] ?? "") },
() => next(),
);
});
Hybrid for high-QPS gateways
| Layer | Carrier |
|---|---|
| Edge auth / HTTP middleware | ALS OK |
| ORM / HTTP client wrappers | read ALS once, pass explicit |
| Pure compute / codecs | explicit args only |
| Worker threads | structured clone of ctx snapshot |
Avoid multiple ALS instances (OpenTelemetry + app + logger each nesting). Prefer one store and diagnostic channels for library metrics — see also undici pools.
Closing checklist
✅ Dos
– ✅ Bench ALS on your Node version and hardware
– ✅ Single store for request correlation
– ✅ Explicit args on ultra-hot pure functions
– ✅ Snapshot ctx when crossing into worker threads
– ✅ Keep getStore() off the per-byte codec path
❌ Don’ts
– ❌ Don’t nest a new ALS per dependency
– ❌ Don’t ban ALS globally without numbers
– ❌ Don’t forget lost context across poorly promisified callbacks
– ❌ Don’t store huge objects in ALS (retainers → heap pressure)
Related reading
- Node.js Event Loop Lag p99
- OpenTelemetry for LLMs
- Lambda Powertools Node Structured Logs
- Deterministic Replay: Agent Sessions
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
