Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Request context becomes infrastructure when more than one subsystem needs the same per-request facts and none of them should have to be handed those facts by hand. In a multi-tenant Node.js service, that usually means a correlation ID for logs, a trace for observability, an authenticated principal, and a tenant scope for data access, all of which must stay consistent from the first middleware to the last background job.
The mechanism that makes this practical is AsyncLocalStorage. The principle that keeps it from becoming dangerous is simple: propagation carries state; it does not validate or authorize that state. Putting a tenant ID into an async store makes it available everywhere. It does not make it correct, and it does not make any query, cache read or queue consumer tenant-safe. This article covers how to build the context, where it breaks, how OpenTelemetry’s context relates to it, and where isolation actually has to be enforced.
How do I share request context across async calls in Node.js?
Use AsyncLocalStorage from node:async_hooks. Node’s asynchronous context tracking documentation describes it as a way to associate state with callbacks and promise chains, so the state stays available for the lifetime of a web request or another asynchronous operation. Node documents the class as stable since v16.4.0. The documentation page current at the time of writing is labeled v26.10.0, but that is the page’s version label, not a minimum runtime requirement.
Node’s own example stores a request ID inside run() and logs it from both synchronous code and a setImmediate() callback for two concurrent HTTP requests. Each request sees its own ID without it being passed as a parameter. That is the practical value: downstream functions can read execution-scoped metadata without every signature in the call stack growing an extra argument.
#1 Best Overall
The documentation also says which implementation to use:
“While you can create your own implementation on top of the
node:async_hooksmodule,AsyncLocalStorageshould be preferred as it is a performant and memory safe implementation that involves significant optimizations that are non-obvious to implement.”
That sentence supports not hand-rolling a store on top of async_hooks. It does not support any particular performance figure, and none is claimed here.
Why request context turns into infrastructure
This is an architectural inference, not a phrase from Node or OpenTelemetry. A request-scoped value starts as a convenience, typically a request ID in log lines. Then other consumers attach themselves:
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →- the logger reads the correlation ID and tenant for every line;
- the tracing layer reads the active span to create children;
- the data layer reads the tenant scope to constrain queries;
- authorization helpers read the principal;
- queue producers and outbound HTTP clients read context to hand it to the next process.
Once that happens, the context is a contract between independent components. Someone has to own when it is created, which fields exist, what each field’s trust level is, what happens when it is absent, and how it crosses a process boundary. Those are infrastructure questions: ownership, initialization, validation, naming, lifecycle, error behavior and propagation rules. Left informal, each module invents its own answer, and the tenant-bearing parts are exactly where an inconsistency becomes a data exposure.
What context carries and what it cannot do
The most useful mental model is that request context is a carrier of verified facts, not a policy decision. The table separates what ambient context contributes from what must be enforced elsewhere.
Rank #2
| Concern | What request context can supply | What still has to enforce it |
|---|---|---|
| Identity | A reference to the authenticated principal | The authentication step that verified it, and authorization checks on each resource |
| Tenant scope | A tenant identifier already verified against the principal’s membership | Database isolation (schema, database, row-level controls or query predicates), authorization on tenant-owned resources |
| Logging | Correlation ID and tenant label on every line | Log policy: what must never be logged |
| Tracing | Trace and span identity for correlation | Trust decisions about incoming trace headers; instrumentation |
| Caching | The tenant to include in keys | Authorization before a protected cache read |
| Async work | Tenant and origin to place on the job | A trusted producer path and re-established authorization in the consumer |
Everything in the right-hand column is guidance drawn from OWASP’s multi-tenant security guidance; none of it comes from the runtime itself.
Should I use AsyncLocalStorage for tenant context?
Yes, as a delivery mechanism for a tenant scope you have already verified, and no, as anything resembling a security control. It is a reasonable way to avoid threading a tenant argument through every layer, and it makes it hard for a repository function to be called without a scope if you make the accessor throw when none exists. What it cannot do is decide that the scope is legitimate or stop a query that ignores it.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallEstablishing tenant context: order of operations
OWASP’s guidance is to establish tenant context early, bind it to server-verified identity and current tenant membership (or service authorization for machine callers), and never treat a client-supplied tenant ID as proof of authorization. A subdomain, header or route parameter can select a tenant; the server must verify that the authenticated subject is allowed to act in it.
That fixes the order. The store must be created after authentication evidence exists and before any tenant-scoped work begins:
- Authenticate the caller. Validate the session or token with your normal mechanism.
- Resolve the tenant. Read the selector (subdomain, route, header) or the tenant claim, then check it against the principal’s current membership or the service’s authorization. A stale claim in a long-lived token is not the same as current membership; decide deliberately how fresh the check must be.
- Fail closed. On a tenant-scoped route, a missing, malformed or unauthorized tenant ends the request with an error before any handler runs. Public or intentionally global routes simply do not get a tenant; do not invent one.
- Open the store with
run(). Everything downstream executes inside the callback. - Treat cross-tenant administration as its own path. It needs separate authorization and auditing, and should be an explicit, visibly different context rather than a tenant ID that someone sets to a different value.
The following is an illustrative sketch of the shape, not a tested implementation. The functions authenticate and resolveTenantForPrincipal stand for your own code.
import { AsyncLocalStorage } from 'node:async_hooks';
const storage = new AsyncLocalStorage();
export function runWithContext(ctx, fn) {
return storage.run(Object.freeze({ ...ctx }), fn);
}
export function requestContext() {
const ctx = storage.getStore();
if (!ctx) throw new Error('No request context: called outside a request scope');
return ctx;
}
// Express-style middleware, mounted after authentication
app.use(authenticate);
app.use(async (req, res, next) => {
try {
const tenantId = await resolveTenantForPrincipal(
req.auth,
req.get('x-tenant-id') // a selector, not proof
);
if (!tenantId) return res.status(403).end();
runWithContext(
{ requestId: req.id, principalId: req.auth.sub, tenantId },
next
);
} catch (err) {
next(err);
}
});
Calling next inside the run() callback is what scopes the rest of the handler chain. Verify this against the middleware framework you use, since frameworks differ in how they invoke later handlers.
Rank #3
Designing the store itself
Keep the schema small, typed and owned by one module. A reasonable set is a correlation ID, an authenticated principal reference, a verified tenant identifier and minimal request metadata. Specific guidance:
- No secrets. Do not store bearer tokens, API keys or unnecessary personal data in a general-purpose ambient context. Anything in it can end up in logs, error reports and span attributes.
- Mediate access. Expose a small accessor (like
requestContext()above) instead of the raw storage object, so arbitrary modules cannot mutate shared state. OpenTelemetry’s context specification takes a similar line, recommending opaque unique keys and mediated access. - Prefer immutability. The same specification says: “A
ContextMUST be immutable, and its write operations MUST result in the creation of a newContextcontaining the original values and the specified values updated.” That is a specification for OpenTelemetry’s own Context, not a rule forAsyncLocalStorage, but it is a sound discipline for a tenant store: freeze it and derive a new scope for a changed value. - Label trust. A tenant ID verified against membership and a raw header value are different things. Only the verified one belongs in the store under the name
tenantId. - Prefer
run()toenterWith().run(store, callback)bounds the scope visibly.enterWith()changes the store for the remainder of the current synchronous execution and its asynchronous continuations, which makes the boundary harder to see. Check Node’s documentation for the semantics in your runtime version before using it.
How do I prevent cross-tenant data leaks in a Node.js app?
By enforcing scope at every resource that holds tenant data, independently of whether the caller remembered to look at the context. OWASP checks authorization at every path through which tenant-owned resources are reached and recommends testing negative cross-tenant cases. The context makes the verified scope available to those controls; it is not a substitute for them.
Choosing a database isolation model
OWASP describes separate databases, separate schemas, shared tables with row-level controls, and hybrids. It does not name a universal winner. How well each works depends on actual enforcement: credentials, roles, policy coverage and operational setup. The comparison axes that matter:
| Axis | What to ask |
|---|---|
| Security boundary | Which component enforces separation, and which credentials or privileged roles can bypass it? |
| Operational complexity | How hard are provisioning, migrations, connection pooling, backups and tenant offboarding? |
| Failure impact | What happens if a predicate is missing, a policy is misconfigured or a cache key omits the tenant? |
| Workload and compliance fit | What is the data classification, any regulatory constraint, and the resource profile of your biggest tenants? |
| Verification burden | Can you inventory every control and test cross-tenant denial continuously? |
None of the designs is secure on its own account. Separate databases still fail if one shared credential can reach all of them. Row-level security still fails if the application role bypasses it or the policy does not cover a table.
Pooled connections and row-level security
For shared PostgreSQL tables protected by row-level security that reads a tenant setting, OWASP recommends transaction-local state, re-established for every transaction. The hazard is connection reuse: a pooled connection carries session state from one request to the next, so a tenant setting that survives the transaction can apply to a different tenant’s later request. The context in AsyncLocalStorage is per request; the connection is shared, so the two lifecycles do not line up unless you make them.
A sketch of the pattern, again illustrative: open a transaction, set the tenant for that transaction only, run the work, commit. In PostgreSQL, set_config(name, value, true) (or SET LOCAL) scopes the setting to the current transaction.
Rank #4
export async function withTenantTx(pool, work) {
const { tenantId } = requestContext();
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
const result = await work(client);
await client.query('COMMIT');
return result;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
Funnelling all tenant-scoped queries through one helper like this also makes the “no context” failure explicit, because requestContext() throws rather than silently running unscoped.
Caches
Include tenant identity in cache keys whenever a value, or an authorization result, varies by tenant. Treat that as defense in depth: separate keys stop one tenant being served another’s entry, but they do not replace an authorization check before a protected cache read.
Queues and background work
AsyncLocalStorage context does not travel through a message broker, and a consumer should not trust it to. OWASP’s approach is to classify each kind of job as tenant-scoped, global or explicitly cross-tenant, bind the tenant scope through a trusted producer path, and re-establish authorization at the consumer. In practice the producer copies the verified tenant ID into the job payload or metadata; the worker reads it, validates that the job type is allowed to act for that tenant, and then opens a fresh run() scope before executing.
Why is AsyncLocalStorage context undefined after await?
Node says AsyncLocalStorage works without issues in most cases and that context loss occurs in rare situations, so do not assume it is a routine problem. When getStore() returns undefined where you expect a value, work through these in order:
- Check you are inside a
run()callback at all. The most common cause of an empty store is code executing before or outside the scope: module initialization, a timer started at startup, an event emitter whose listener was registered elsewhere, or a handler mounted before the context middleware. - Locate the exact operation where it disappears. Log
getStore()before and after each suspected call. Node’s guidance is to check the suspected calls rather than guess. - Look for callback-based APIs. Node notes these can be promisified, which often keeps the context intact.
- Use
AsyncResourcefor custom callback work. If a library queues callbacks itself (a custom pool or queue, for example),AsyncResourcecan associate the callback with the right execution context. - Consider custom thenables. Node’s documentation lists these among the situations where context can be lost.
The deeper lesson is the failure mode. Because the store is empty rather than wrong, the accessor should throw on a missing context in tenant-scoped code. A silent fallback to “no tenant” is how a context bug becomes a leak.
Does OpenTelemetry context carry my tenant ID?
Not unless you put it there, and it should not be your tenant authority if you do. OpenTelemetry’s context is related to your request context but is a separate system with a different purpose.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteActive context and the context manager
The OpenTelemetry JavaScript Context API stores the active span so that components creating child spans can find the parent. The active context depends on a configured context manager. The JavaScript documentation is explicit: “Without one, api.context.active() will ALWAYS return the ROOT_CONTEXT.” In Node, a context manager can use async_hooks or AsyncLocalStorage for the execution propagation, which is why the two get conflated. Having your own tenant store and OpenTelemetry’s context manager in the same process is normal; they are separate stores.
Propagation between services
OpenTelemetry propagation moves context between services by injecting values into a carrier (such as HTTP headers) on the sender and extracting them at the receiver. Supported instrumentation handles most common cases automatically; manual propagation is for gaps where no matching instrumentation exists or you need behavior it does not provide. The default propagator uses W3C TraceContext headers.
Trace identity is not tenant identity
A trace ID establishes causal correlation across spans and services. It says nothing about whether the caller belongs to a given tenant. Likewise a tenant-id header that travels beside traceparent or baggage inherits no trust from it. OpenTelemetry advises caution with externally supplied context and limiting sensitive internal information sent to untrusted services. Baggage in particular must never carry credentials, API keys or personal data, because it can be forwarded to downstream services and observed there.
If you want tenant labels on spans, set them as attributes from your verified context inside the service. For service-to-service calls, the receiving service should derive tenant scope from its own authentication of the caller, and treat any propagated tenant hint as a selector to be checked, exactly like a client header.
A testing checklist
This is guidance synthesized from Node’s and OWASP’s documentation, not a record of tests run against any particular system. Isolation is something you verify, because a missed predicate does not fail loudly.
- Async boundaries. Assert that the store survives promise chains,
await, timers and any callback-based library you use, and that it is absent where it should be (module startup, unscoped workers). - Concurrency. Fire concurrent requests for two tenants with deliberately interleaved delays; assert each sees only its own context and data.
- Client-supplied tenant. Send a valid user’s token with another tenant’s identifier in the header or route; expect denial, not an empty result.
- Missing context. Call a tenant-scoped repository function outside a request scope and expect an error.
- Connection reuse. With a small pool, run tenant A’s transaction, then tenant B’s on the same connection; confirm no tenant setting leaks. Use the actual request database role, and confirm that role cannot bypass row security.
- Positive and negative cases. Test successful same-tenant operations as well as denied cross-tenant ones, on every path to tenant-owned resources.
- Caches. Confirm keys vary by tenant and a protected read still passes authorization.
- Consumers. Enqueue a job claiming a tenant the producer was not authorized for; the consumer should reject it.
Where this leaves the design
Build one small, owned, immutable request context; populate it only after authentication and tenant verification; make its accessor fail when it is empty; let OpenTelemetry handle trace propagation with its own context manager; and enforce tenant scope again at the database, cache, storage and queue layers. The context is the shared infrastructure. The enforcement at each resource is the isolation.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




