AsyncLocalStorage permite armazenar e recuperar contexto durante uma cadeia assíncrona no Node.js. Ele é usado para manter request ID, trace ID, identidade técnica do tenant e outras informações operacionais sem passar parâmetros por todas as funções.
A API pertence a node:async_hooks, mas oferece uma interface de alto nível. Para correlação de logs e tracing em aplicações, normalmente é mais segura e simples que implementar mapas de asyncId manualmente.
Problema que resolve
Considere uma requisição que atravessa controller, serviço, repositório e cliente HTTP. Sem contexto assíncrono, cada camada precisa receber o request ID:
await service.execute({ requestId, userId });
await repository.save({ requestId, data });Com AsyncLocalStorage, o ID acompanha o fluxo:
import { AsyncLocalStorage } from 'node:async_hooks';
export const requestContext = new AsyncLocalStorage();Criando contexto por requisição
import { randomUUID } from 'node:crypto';
import express from 'express';
import { requestContext } from './request-context.js';
const app = express();
app.use((req, res, next) => {
const store = {
requestId: req.get('x-request-id') || randomUUID(),
method: req.method,
path: req.path,
};
requestContext.run(store, next);
});run cria um contexto para o callback e para operações assíncronas iniciadas dentro dele. Ao terminar, o contexto anterior é restaurado.
Lendo o store
export function getRequestContext() {
return requestContext.getStore();
}
export function logInfo(message, data = {}) {
const context = getRequestContext();
logger.info({
...data,
requestId: context?.requestId,
}, message);
}Trate a ausência de store. Scripts, jobs, inicialização e testes podem chamar funções fora de uma requisição.
run e enterWith
run limita o contexto a um callback e restaura o estado depois. enterWith altera o contexto para o restante da execução síncrona atual e continua em operações seguintes. Em aplicações web, prefira run, pois o limite explícito reduz vazamento acidental entre eventos.
requestContext.run({ requestId: 'abc' }, async () => {
await execute();
});Store imutável
Evite transformar o store em um objeto global mutável. Defina os campos na entrada e crie novos objetos quando precisar de escopo adicional:
function withOperation(name, callback) {
const current = requestContext.getStore() || {};
return requestContext.run({ ...current, operation: name }, callback);
}Isso impede que uma função altere silenciosamente o contexto usado por outras operações concorrentes.
Integração com logger
export function childLogger() {
const store = requestContext.getStore();
return logger.child({
requestId: store?.requestId,
traceId: store?.traceId,
tenantId: store?.tenantId,
});
}Não inclua tokens, senhas, payloads completos ou dados pessoais. Contexto propagado deve ser pequeno e operacional.
Propagando um request ID
async function callPayments(path, options = {}) {
const store = requestContext.getStore();
return fetch(`https://payments.internal${path}`, {
...options,
headers: {
...options.headers,
'x-request-id': store?.requestId || randomUUID(),
},
});
}Valide IDs recebidos e limite tamanho e caracteres. Um header externo não deve ser confiado sem normalização.
Tracing distribuído
OpenTelemetry mantém contexto de spans por mecanismos assíncronos. Não crie dois sistemas concorrentes sem necessidade. Quando já existe tracing, extraia trace ID do span ativo para logs.
import { trace } from '@opentelemetry/api';
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;AsyncLocalStorage continua útil para campos de aplicação que não pertencem ao padrão de tracing.
Transações de banco
Algumas arquiteturas armazenam a transação atual no contexto para que repositórios participem dela:
const transactionContext = new AsyncLocalStorage();
async function withTransaction(callback) {
return database.transaction(async (transaction) => {
return transactionContext.run({ transaction }, callback);
});
}
function currentTransaction() {
return transactionContext.getStore()?.transaction;
}Use com cautela. Dependências implícitas dificultam testes e podem esconder limites transacionais. Documente claramente quais funções exigem contexto.
Jobs e consumidores
Crie um novo contexto para cada mensagem:
async function consume(message) {
const store = {
jobId: message.id,
correlationId: message.correlationId || randomUUID(),
};
return requestContext.run(store, async () => {
await processMessage(message);
});
}Nunca reutilize o mesmo store mutável entre mensagens concorrentes.
Timers e tarefas agendadas
Operações criadas dentro de run herdam o contexto. Isso pode ser indesejado para timers de longa duração. Se uma tarefa pertence a outro ciclo, crie um contexto novo ou remova a dependência.
Quando o contexto se perde
Bibliotecas que implementam filas de callbacks de forma não convencional podem quebrar a propagação. Use AsyncResource na biblioteca ou envolva o callback:
import { AsyncResource } from 'node:async_hooks';
function bindCallback(callback) {
const resource = new AsyncResource('BoundCallback');
return (...args) => {
try {
return resource.runInAsyncScope(callback, null, ...args);
} finally {
resource.emitDestroy();
}
};
}Antes de criar wrappers, confirme com um teste mínimo onde o store desaparece.
snapshot e bind
Versões recentes do Node.js oferecem utilitários para capturar o contexto e executar funções nele. Verifique a versão mínima adotada pelo projeto antes de usar APIs novas e mantenha testes para upgrades.
Worker Threads
O contexto não atravessa automaticamente a fronteira de uma Worker Thread. Envie apenas campos necessários:
worker.postMessage({
task,
context: {
requestId: requestContext.getStore()?.requestId,
},
});No worker, crie seu próprio AsyncLocalStorage para cada mensagem.
Testes
import assert from 'node:assert/strict';
await requestContext.run({ requestId: 'test-1' }, async () => {
await Promise.resolve();
assert.equal(requestContext.getStore()?.requestId, 'test-1');
});
assert.equal(requestContext.getStore(), undefined);Teste promises, timers, chamadas do framework e bibliotecas críticas. Também confirme que uma requisição não enxerga o contexto de outra.
Limpeza e disable
O contexto de run é delimitado automaticamente. disable deve ser usado quando a instância não será mais necessária, por exemplo no encerramento de uma aplicação ou em testes que criam instâncias temporárias.
Overhead
AsyncLocalStorage tem custo. Meça em rotas de alto volume com e sem contexto. Mantenha o store pequeno, evite mutações frequentes e não crie várias instâncias sem necessidade.
Erros comuns
- usar um store global compartilhado;
- armazenar objetos grandes ou sensíveis;
- assumir que getStore sempre retorna valor;
- usar enterWith em eventos compartilhados;
- não testar bibliotecas que quebram contexto;
- propagar contexto inteiro para workers ou serviços;
- esconder dependências de negócio no store;
- ignorar overhead sob carga;
- confiar em request ID externo sem validar.
Fluxo recomendado
- defina campos mínimos;
- crie contexto na entrada da requisição ou job;
- use
runpara delimitar o escopo; - leia o store em logs e observabilidade;
- propague apenas identificadores necessários;
- teste concorrência e bibliotecas críticas;
- meça overhead;
- documente dependências implícitas.
Combine AsyncLocalStorage com Async Hooks, medições em perf_hooks e tracing com OpenTelemetry no Node.js.
Consulte a documentação oficial de AsyncLocalStorage e a referência oficial de AsyncResource.




