A request id belongs in every log line the request produces, including lines written by a database driver four calls down. Threading a context object through every signature works until one function in the chain belongs to a library. AsyncLocalStorage inverts it: run the handler inside a store, and any code reached from it — after any number of awaits, timers or callbacks — reads it back with getStore().
import { AsyncLocalStorage } from 'node:async_hooks';
import { createServer } from 'node:http';
import { once } from 'node:events';
import { setTimeout as delay } from 'node:timers/promises';
const als = new AsyncLocalStorage({ name: 'request' });
const log = (m) => console.log(`[${als.getStore()?.id ?? '--------'}] ${m}`);
async function loadUser(id) { // no context parameter anywhere in this chain
await delay(20 * Number(id));
log(`db: user ${id} loaded`);
return { id: Number(id) };
}
let seq = 0;
const server = createServer((req, res) => als.run({ id: `req-${++seq}` }, async () => {
log(`start ${req.method} ${req.url}`);
res.end(JSON.stringify(await loadUser(req.url.slice(1))));
setTimeout(() => log('metrics flushed after the response'), 5);
}));
server.listen(0, '127.0.0.1'); await once(server, 'listening');
const base = `http://127.0.0.1:${server.address().port}`;
await Promise.all([fetch(`${base}/2`), fetch(`${base}/1`)]);
await delay(20); log('back on the main line, no store'); server.close();[req-1] start GET /2 [req-2] start GET /1 [req-2] db: user 1 loaded [req-1] db: user 2 loaded [req-2] metrics flushed after the response [req-1] metrics flushed after the response [--------] back on the main line, no store
The requests interleave — the slower one started first and finished last — yet no line carries the wrong id, and the timer that runs after the response still sees its store. A module-level variable would hold req-2 for both. async_hooks, the mechanism underneath, gives every asynchronous operation an id and a parent id.

run() and getStore() are stable, as are AsyncLocalStorage.bind(fn) and AsyncLocalStorage.snapshot(), which capture the current context so a callback registered during a request can re-enter it later from a shared pool or a long-lived emitter. enterWith(), exit() and disable() remain Stability 1 - Experimental. Keep the store shallow — an id, a user, a logger — because everything in it stays reachable until the request's last callback finishes.