AsyncLocalStorage no Node.js permite manter um contexto associado a uma cadeia de operações assíncronas. Em uma API, isso significa que um identificador de requisição, o tenant atual, o usuário autenticado ou informações de tracing podem continuar disponíveis em funções profundas sem precisar ser passados manualmente em todos os parâmetros.
O recurso faz parte do módulo nativo node:async_hooks e é a opção recomendada para armazenamento local assíncrono. Ele ajuda a criar logs correlacionados, auditoria, métricas, rastreamento distribuído e isolamento de contexto, mas exige atenção a ciclo de vida, concorrência, bibliotecas de terceiros e segurança dos dados armazenados.
Neste guia, você aprenderá como criar um store por requisição, usar run(), enterWith(), getStore() e snapshot(), integrar com Express ou Fastify, propagar request IDs, evitar vazamentos entre usuários e testar o comportamento.
O que é AsyncLocalStorage?
AsyncLocalStorage cria um armazenamento que acompanha callbacks, promises, timers e muitas outras operações assíncronas iniciadas dentro de um contexto. A documentação oficial de contexto assíncrono do Node.js descreve a API e suas garantias.
Ele é construído sobre a infraestrutura de async hooks, mas evita a complexidade de implementar rastreamento manual. Para entender a base técnica, consulte também Async Hooks no Node.js.
Por que usar contexto assíncrono?
Uma requisição HTTP percorre middleware, serviços, repositórios, filas internas e clientes externos. Sem um contexto compartilhado, cada função precisa receber dados auxiliares:
await processOrder({
orderId,
requestId,
userId,
tenantId
});Com AsyncLocalStorage, os dados operacionais podem ser lidos quando necessários, sem poluir todas as assinaturas.
Quando não usar
Não transforme o store em um contêiner global de dependências. Dados essenciais para a regra de negócio devem continuar explícitos. O contexto é mais adequado para metadados transversais, como correlação, tracing e auditoria.
Criando a instância
import { AsyncLocalStorage } from 'node:async_hooks';
export const requestContext = new AsyncLocalStorage();Crie normalmente uma única instância por tipo de contexto da aplicação. Evite criar uma instância nova para cada requisição.
Definindo o store
Use um objeto pequeno e previsível:
const store = {
requestId: crypto.randomUUID(),
userId: null,
tenantId: null,
startedAt: Date.now()
};Não armazene objetos enormes, request completo, response completo ou segredos. Isso aumenta retenção de memória e risco de exposição.
Usando run()
requestContext.run(store, async () => {
await handleRequest();
});run() ativa o store apenas durante a execução do callback e das operações assíncronas iniciadas nele. Ao terminar, o contexto externo é restaurado.
Lendo com getStore()
export function getRequestContext() {
const store = requestContext.getStore();
if (!store) {
throw new Error('Contexto de requisição indisponível');
}
return store;
}Decida se a ausência deve gerar erro ou retornar valores padrão. Em código executado por jobs, scripts e testes, talvez não exista uma requisição HTTP.
Middleware no Express
import crypto from 'node:crypto';
app.use((req, res, next) => {
const incomingId = req.get('x-request-id');
const requestId = validateRequestId(incomingId)
? incomingId
: crypto.randomUUID();
const store = {
requestId,
userId: null,
tenantId: null,
startedAt: Date.now()
};
requestContext.run(store, () => {
res.setHeader('x-request-id', requestId);
next();
});
});Valide o ID fornecido pelo cliente. Limite tamanho e caracteres para evitar injeção em logs.
Middleware assíncrono
O callback passado a run() pode iniciar uma promise:
app.use((req, res, next) => {
requestContext.run(createStore(req), async () => {
try {
await loadIdentity(req);
next();
} catch (error) {
next(error);
}
});
});Adicionando identidade
Você pode preencher campos depois da autenticação:
const store = requestContext.getStore();
if (store) {
store.userId = user.id;
store.tenantId = user.tenantId;
}Prefira um formato controlado. Não copie o objeto inteiro do usuário.
Logs correlacionados
export function logInfo(message, fields = {}) {
const context = requestContext.getStore();
logger.info({
...fields,
requestId: context?.requestId,
userId: context?.userId,
tenantId: context?.tenantId
}, message);
}Essa integração funciona bem com logs com Pino no Node.js e logs com Winston no Node.js.
Redação de dados
Request ID e IDs internos normalmente são aceitáveis, mas tokens, cookies, senhas e dados pessoais sensíveis não devem entrar no store nem nos logs. Configure redaction no logger.
Tracing distribuído
Bibliotecas de tracing usam mecanismos semelhantes para propagar spans. Ao integrar com OpenTelemetry, evite manter duas fontes conflitantes para o mesmo trace ID. Consulte OpenTelemetry no Node.js.
Tenant atual
Em aplicações multi-tenant, o tenant pode ser armazenado no contexto para logs e observabilidade. Entretanto, a autorização e as consultas ao banco não devem depender apenas de um valor implícito.
Valide o tenant na camada de autenticação e aplique controles no banco. Veja Multi-Tenancy no Node.js e Row-Level Security no Node.js.
run() versus enterWith()
run() cria um limite explícito e restaura o contexto anterior. enterWith() altera o contexto para o restante da execução síncrona atual e pode afetar handlers posteriores:
requestContext.enterWith(store);Em servidores HTTP, prefira run(). Use enterWith() apenas quando compreender claramente o escopo.
Contexto em EventEmitter
Listeners registrados em momentos diferentes podem executar no contexto associado ao recurso que dispara o evento, não necessariamente ao que você espera. Registre listeners dentro do contexto ou use AsyncResource quando integrar APIs próprias.
Para casos avançados, consulte AsyncResource no Node.js.
Callbacks de bibliotecas antigas
Uma biblioteca que cria recursos assíncronos de forma incompatível pode perder o contexto. Primeiro identifique o ponto com logs e testes. Depois envolva a integração com AsyncResource ou atualize a dependência.
Promises
Promises nativas normalmente preservam o contexto:
requestContext.run(store, async () => {
await Promise.resolve();
console.log(requestContext.getStore()?.requestId);
});Timers
requestContext.run(store, () => {
setTimeout(() => {
console.log(requestContext.getStore()?.requestId);
}, 10);
});O timer criado dentro do contexto normalmente mantém o store.
Tarefas em background
Evite iniciar tarefas desacopladas que mantêm o contexto da requisição por muito tempo:
void processLargeJob();Para jobs persistentes, envie apenas os identificadores necessários para uma fila e crie um novo contexto no worker.
Contexto em filas
Inclua um correlation ID na mensagem. No consumidor:
requestContext.run({
requestId: message.correlationId,
jobId: message.id
}, async () => {
await processMessage(message);
});snapshot()
Versões modernas do Node.js oferecem AsyncLocalStorage.snapshot() para capturar o contexto atual e executar funções posteriormente nele:
const runInContext = AsyncLocalStorage.snapshot();
setTimeout(() => {
runInContext(() => {
console.log(requestContext.getStore());
});
}, 10);Verifique a versão do Node.js usada pela aplicação antes de depender dessa API.
bind()
AsyncLocalStorage.bind() vincula uma função ao contexto atual. É útil quando uma API recebe callback que será chamado depois.
disable()
disable() desativa a instância e permite que ela seja coletada quando não for mais usada. Não chame após cada requisição. Use apenas no encerramento de uma instância descartável ou em testes específicos.
Memória
O store fica alcançável enquanto existirem recursos assíncronos associados. Objetos grandes, buffers e árvores completas de requisição podem aumentar o uso de memória.
Vazamento de contexto
Um vazamento lógico ocorre quando dados de uma requisição aparecem em outra. Isso pode acontecer ao reutilizar um objeto mutável global:
const sharedStore = {};
app.use((req, res, next) => {
sharedStore.requestId = crypto.randomUUID();
requestContext.run(sharedStore, next);
});Crie um objeto novo por requisição.
Não use variável global
Uma variável global currentUser é insegura porque várias requisições executam concorrentemente no mesmo processo. AsyncLocalStorage existe justamente para separar esses fluxos.
Objetos imutáveis
Você pode congelar o store inicial e usar objetos derivados, mas isso dificulta adicionar identidade depois. Outra opção é separar campos imutáveis de um pequeno estado controlado.
Validação de request ID
function validateRequestId(value) {
return typeof value === 'string'
&& value.length <= 100
&& /^[a-zA-Z0-9._:-]+$/.test(value);
}Erros
O contexto continua disponível durante o tratamento de erros iniciado na mesma cadeia. Isso permite registrar request ID no middleware central:
app.use((error, req, res, next) => {
logger.error({
err: error,
requestId: requestContext.getStore()?.requestId
}, 'Falha na requisição');
res.status(500).json({
error: 'internal_error',
requestId: requestContext.getStore()?.requestId
});
});Resposta ao cliente
Retornar o request ID facilita suporte, mas não exponha IDs internos de usuário, tenant ou trace detalhado.
Testes unitários
test('expõe o requestId dentro do contexto', async () => {
await requestContext.run({ requestId: 'req-123' }, async () => {
await Promise.resolve();
assert.equal(getRequestContext().requestId, 'req-123');
});
});Teste de isolamento
test('isola requisições concorrentes', async () => {
const execute = (requestId, delay) =>
requestContext.run({ requestId }, async () => {
await setTimeout(delay);
return requestContext.getStore().requestId;
});
const values = await Promise.all([
execute('a', 20),
execute('b', 5)
]);
assert.deepEqual(values, ['a', 'b']);
});Teste de ausência
Confirme o comportamento quando uma função é executada fora do contexto. Isso evita falhas inesperadas em scripts e jobs.
Teste com servidor real
Envie várias requisições simultâneas com IDs diferentes e confirme que logs e respostas nunca misturam os valores.
Benchmark
AsyncLocalStorage adiciona overhead. Meça com a carga real, quantidade de awaits, logger e instrumentação. Na maioria das APIs, a utilidade de correlação supera o custo, mas hot paths intensivos merecem teste.
Observabilidade
Monitore latência, event loop, uso de memória e quantidade de recursos. Diagnostics Channel no Node.js pode complementar instrumentações com baixo acoplamento.
Compatibilidade
Use uma versão do Node.js em manutenção e consulte a estabilidade de cada método. A página oficial de versões do Node.js ajuda a escolher uma linha suportada.
Erros comuns
- Store global reutilizado: mistura dados entre requisições.
- Guardar request completo: aumenta memória e exposição.
- Usar enterWith sem escopo: contamina handlers posteriores.
- Confiar no tenant implícito: enfraquece autorização.
- Não validar request ID: permite conteúdo malformado nos logs.
- Job desacoplado herdando contexto: retém dados desnecessários.
- Ignorar testes concorrentes: vazamentos aparecem apenas sob carga.
Boas práticas
- Crie um store novo por requisição.
- Prefira
run(). - Mantenha o store pequeno.
- Não armazene segredos.
- Valide IDs recebidos.
- Use contexto para metadados, não para regras essenciais.
- Crie novo contexto em workers.
- Teste concorrência.
- Meça overhead.
- Integre com logs e traces de forma consistente.
Conclusão
Usar AsyncLocalStorage no Node.js simplifica a propagação de request IDs, informações de tracing e outros metadados em aplicações assíncronas. O recurso evita parâmetros repetitivos e melhora a correlação de logs, erros e métricas.
A implementação segura depende de um store pequeno, criado por requisição, ativado com run() e tratado como contexto operacional. Com validação, testes concorrentes e integração cuidadosa com filas e bibliotecas, AsyncLocalStorage fornece uma base sólida para observabilidade sem misturar dados entre usuários.




