Em uma aplicação Node.js, uma única requisição pode atravessar middlewares, serviços, consultas ao banco, chamadas HTTP e filas internas antes de produzir uma resposta. Passar manualmente um identificador por todas essas funções deixa o código repetitivo e fácil de quebrar. O AsyncLocalStorage no Node.js resolve esse problema ao manter um contexto associado ao fluxo assíncrono atual.
Com ele, você pode armazenar dados como requestId, usuário autenticado, origem da chamada e informações de rastreamento. Esses valores continuam disponíveis em Promises, timers e outras operações criadas dentro do contexto, sem precisar adicionar parâmetros a todas as funções.
Neste guia, você aprenderá a criar um armazenamento assíncrono, integrar o recurso ao Express, produzir logs estruturados, evitar vazamento de dados entre requisições, lidar com perda de contexto, testar a implementação e decidir quando usar AsyncResource.
O que é AsyncLocalStorage?
AsyncLocalStorage é uma classe do módulo nativo node:async_hooks. Ela permite associar um valor a uma execução assíncrona e recuperar esse valor em funções chamadas posteriormente dentro da mesma cadeia.
O comportamento lembra o armazenamento local por thread existente em outras linguagens. A diferença é que o Node.js normalmente executa o JavaScript da aplicação em uma thread principal e alterna entre várias operações assíncronas. O recurso acompanha essas relações para entregar o armazenamento correto em cada fluxo.
Antes de avançar, revise o que é Node.js, o que é JavaScript e o guia para criar uma API com Node.js.
A documentação oficial de contexto assíncrono do Node.js recomenda AsyncLocalStorage em vez de implementações manuais baseadas diretamente nos hooks internos, pois a classe inclui otimizações e cuidados de segurança de memória.
Por que passar contexto manualmente é um problema?
Imagine que uma rota receba uma requisição e chame três serviços:
app.get('/orders/:id', async (req, res) => {
const requestId = crypto.randomUUID();
const order = await findOrder(req.params.id, requestId);
const payment = await findPayment(order.id, requestId);
await writeAuditLog(order, payment, requestId);
res.json({ order, payment });
});O identificador precisa aparecer em cada assinatura. Se um novo serviço for adicionado e o parâmetro for esquecido, os logs ficam incompletos. Bibliotecas internas também passam a depender de detalhes da camada HTTP.
Com AsyncLocalStorage, o código da rota cria o contexto uma vez. Funções internas recuperam os dados quando precisam registrar uma mensagem, sem receber o identificador explicitamente.
Criando o primeiro armazenamento
Crie um módulo chamado request-context.js:
const { AsyncLocalStorage } = require('node:async_hooks');
const requestContext = new AsyncLocalStorage();
function getContext() {
return requestContext.getStore();
}
module.exports = {
requestContext,
getContext
};Cada instância mantém seu próprio armazenamento. É possível ter uma instância para contexto de requisição e outra para uma finalidade diferente, mas uma única estrutura bem definida costuma ser suficiente para a maior parte das APIs.
Entendendo run() e getStore()
O método run(store, callback) executa uma função dentro de um contexto. Todas as operações assíncronas criadas no callback podem recuperar o mesmo armazenamento com getStore().
const { requestContext, getContext } = require('./request-context');
requestContext.run({ requestId: 'req-123' }, async () => {
console.log(getContext());
await Promise.resolve();
setTimeout(() => {
console.log(getContext());
}, 10);
});Fora do callback e de sua cadeia assíncrona, getStore() retorna undefined. Por isso, funções reutilizáveis devem lidar com a ausência do contexto, especialmente quando também são executadas por scripts, testes ou tarefas agendadas.
Middleware para Express
Em uma API Express, o local mais comum para criar o contexto é um middleware executado no início da requisição:
const crypto = require('node:crypto');
const express = require('express');
const { requestContext } = require('./request-context');
const app = express();
app.use((req, res, next) => {
const requestId = req.get('X-Request-Id') || crypto.randomUUID();
const store = {
requestId,
method: req.method,
path: req.originalUrl,
startedAt: Date.now()
};
res.setHeader('X-Request-Id', requestId);
requestContext.run(store, next);
});O servidor reaproveita um identificador enviado por uma infraestrutura confiável ou cria um novo. Em ambientes públicos, valide tamanho e formato do cabeçalho antes de reutilizá-lo, pois dados fornecidos pelo cliente não devem ser aceitos sem limites.
O middleware deve ser registrado antes das rotas e dos componentes que dependem do contexto. Caso um middleware anterior crie operações assíncronas relevantes, elas não estarão associadas ao armazenamento iniciado depois.
Criando um logger com requestId
Um logger pode buscar o contexto automaticamente:
const { getContext } = require('./request-context');
function log(level, message, extra = {}) {
const context = getContext() || {};
const entry = {
timestamp: new Date().toISOString(),
level,
message,
requestId: context.requestId,
method: context.method,
path: context.path,
...extra
};
console.log(JSON.stringify(entry));
}
module.exports = { log };Agora qualquer serviço pode registrar mensagens sem receber o identificador:
const { log } = require('./logger');
async function findOrder(orderId) {
log('info', 'Buscando pedido', { orderId });
const order = await database.orders.findById(orderId);
log('info', 'Pedido encontrado', { orderId });
return order;
}Esse padrão melhora a investigação de problemas porque todas as mensagens relacionadas à mesma requisição carregam o mesmo identificador. Para uma estratégia mais ampla de métricas e rastreamento, o contexto também pode ser integrado a ferramentas de observabilidade.
Armazenando usuário e dados de autenticação
Depois que um middleware autentica o usuário, ele pode complementar o armazenamento atual:
const { getContext } = require('./request-context');
app.use(async (req, res, next) => {
const user = await authenticate(req);
const context = getContext();
if (context && user) {
context.userId = user.id;
context.role = user.role;
}
req.user = user;
next();
});O armazenamento normalmente é um objeto mutável. Alterações feitas dentro da mesma requisição ficam disponíveis para operações posteriores. Isso é útil, mas exige disciplina: não guarde senhas, tokens completos, documentos, payloads extensos ou outros dados sensíveis.
Consulte também as práticas de segurança em aplicações web antes de registrar informações de autenticação.
Contextos isolados entre requisições
Duas requisições podem ser processadas ao mesmo tempo, mas cada uma recebe seu próprio armazenamento:
app.get('/demo', async (req, res) => {
const context = getContext();
await new Promise(resolve => {
setTimeout(resolve, Math.random() * 100);
});
res.json({ requestId: context.requestId });
});Mesmo que os timers terminem em ordem diferente, o Node.js recupera o armazenamento associado ao fluxo correto. Nunca substitua essa estrutura por uma variável global comum, pois uma requisição poderia sobrescrever os dados de outra.
Usando Map ou objeto?
O armazenamento pode ser qualquer valor. Objetos são simples e funcionam bem com TypeScript. Um Map também é uma opção:
requestContext.run(new Map([
['requestId', requestId],
['startedAt', Date.now()]
]), next);Com Map, chaves podem ser adicionadas sem colisões acidentais com propriedades herdadas. Com objetos, a leitura é mais direta e a tipagem costuma ser mais confortável. Escolha um formato e mantenha o contrato consistente em todo o projeto.
Tipando o contexto com TypeScript
import { AsyncLocalStorage } from 'node:async_hooks';
export interface RequestContext {
requestId: string;
method: string;
path: string;
startedAt: number;
userId?: string;
}
export const requestContext =
new AsyncLocalStorage<RequestContext>();
export function getContext(): RequestContext | undefined {
return requestContext.getStore();
}Uma função auxiliar pode exigir contexto quando a ausência representa erro de programação:
export function requireContext(): RequestContext {
const context = requestContext.getStore();
if (!context) {
throw new Error('Contexto de requisição indisponível');
}
return context;
}Use essa abordagem apenas em componentes que realmente dependem de uma requisição. Um serviço compartilhado com scripts ou consumidores de fila pode precisar aceitar a ausência e criar um contexto próprio.
Contexto em tarefas de fila
Uma tarefa executada depois da resposta HTTP não deve depender do contexto original ainda existir. Inclua no payload apenas os dados necessários e crie um novo contexto no consumidor:
async function processJob(job) {
const store = {
requestId: job.requestId || crypto.randomUUID(),
jobId: job.id,
source: 'queue',
startedAt: Date.now()
};
return requestContext.run(store, async () => {
log('info', 'Iniciando tarefa');
await executeJob(job);
log('info', 'Tarefa concluída');
});
}Para conhecer estruturas de cache e filas, veja o guia de Redis com Node.js.
run() ou enterWith()?
run() cria um escopo claro e restaura o contexto anterior depois do callback. O método enterWith() altera o contexto da execução síncrona atual e o propaga para chamadas assíncronas posteriores.
Na maioria das aplicações, prefira run(). Um enterWith() usado dentro de um evento pode fazer outros listeners do mesmo evento enxergarem o armazenamento, produzindo efeitos difíceis de perceber.
requestContext.run(store, () => {
next();
});Use enterWith() somente quando a duração e as consequências da mudança estiverem bem compreendidas.
bind() e snapshot()
Versões atuais do Node.js oferecem métodos para capturar o contexto de execução. AsyncLocalStorage.bind() devolve uma função vinculada ao contexto atual. AsyncLocalStorage.snapshot() cria uma função capaz de executar callbacks dentro do contexto capturado.
const runInCapturedContext = requestContext.run(store, () => {
return AsyncLocalStorage.snapshot();
});
setTimeout(() => {
runInCapturedContext(() => {
log('info', 'Executando no contexto capturado');
});
}, 100);Esses métodos são úteis quando um callback será chamado por uma camada que não preserva o contexto naturalmente. Verifique a versão mínima de Node.js definida pelo projeto antes de adotá-los.
Por que o contexto pode ser perdido?
Promises nativas, timers e a maior parte das APIs centrais preservam o contexto. Perdas geralmente aparecem em bibliotecas antigas baseadas em callbacks, implementações personalizadas de thenables ou recursos assíncronos criados fora dos mecanismos esperados.
Ao investigar, registre getStore() depois de cada etapa suspeita:
log('debug', 'Antes da biblioteca', {
hasContext: Boolean(getContext())
});
await callLegacyLibrary();
log('debug', 'Depois da biblioteca', {
hasContext: Boolean(getContext())
});Quando uma API baseada em callback possui versão compatível com util.promisify(), transformá-la em Promise nativa pode resolver o problema.
Restaurando contexto com AsyncResource
Quando uma biblioteca ou recurso personalizado perde o vínculo assíncrono, use AsyncResource para executar o callback no escopo correto:
const { AsyncResource } = require('node:async_hooks');
function wrapCallback(callback) {
const resource = new AsyncResource('LegacyCallback');
return (...args) => {
try {
return resource.runInAsyncScope(callback, null, ...args);
} finally {
resource.emitDestroy();
}
};
}A documentação oficial de async_hooks explica os identificadores assíncronos e a classe AsyncResource. Não crie uma camada manual de hooks apenas para substituir o AsyncLocalStorage.
AsyncLocalStorage e Worker Threads
O armazenamento não é compartilhado automaticamente com outra Worker Thread. Uma thread possui seu próprio contexto JavaScript. Envie os campos necessários na mensagem e crie um novo armazenamento dentro do worker.
worker.postMessage({
taskId,
requestId: getContext()?.requestId,
payload
});parentPort.on('message', message => {
const store = {
requestId: message.requestId,
taskId: message.taskId,
source: 'worker'
};
requestContext.run(store, () => {
processTask(message.payload);
});
});Para processamento paralelo e pools, consulte Worker Threads no Node.js.
Medindo duração da requisição
O horário inicial armazenado no middleware pode ser usado no evento finish da resposta:
app.use((req, res, next) => {
const requestId = crypto.randomUUID();
const store = {
requestId,
startedAt: performance.now(),
method: req.method,
path: req.originalUrl
};
requestContext.run(store, () => {
res.on('finish', () => {
log('info', 'Requisição concluída', {
statusCode: res.statusCode,
durationMs: performance.now() - store.startedAt
});
});
next();
});
});Essas medições ajudam a identificar endpoints lentos. Combine-as com as práticas do artigo sobre performance de APIs Node.js.
Cuidados com desempenho e memória
O recurso adiciona trabalho ao rastreamento assíncrono, mas costuma ser adequado para identificadores e metadados pequenos. O impacto real depende do volume de requisições, quantidade de operações assíncronas e bibliotecas utilizadas. Faça testes de carga no ambiente mais próximo possível da produção.
Evite guardar objetos grandes, respostas completas, instâncias de requisição, conexões de banco ou buffers. O armazenamento permanece alcançável enquanto os recursos assíncronos associados estiverem vivos. Um timer esquecido ou uma Promise que nunca termina também pode prolongar a vida dos dados.
Quando uma instância de AsyncLocalStorage não será mais utilizada no processo, disable() permite desligá-la. Em servidores que mantêm a instância durante toda a execução, normalmente não é necessário chamar o método a cada requisição.
Segurança e privacidade
- Não armazene senhas, tokens completos ou dados de cartão.
- Não registre payloads inteiros automaticamente.
- Valide identificadores recebidos por cabeçalhos.
- Use nomes de campos previsíveis e documentados.
- Restrinja o logger para não serializar objetos inesperados.
- Remova ou masque dados pessoais antes de enviar logs a serviços externos.
- Não use o contexto como substituto da autorização.
O fato de um userId estar disponível não garante que o usuário possa acessar um recurso. As verificações de permissão continuam obrigatórias em cada operação sensível.
Como testar AsyncLocalStorage
Os testes devem verificar propagação, isolamento e ausência de contexto:
const assert = require('node:assert/strict');
await Promise.all([
requestContext.run({ requestId: 'A' }, async () => {
await new Promise(resolve => setTimeout(resolve, 20));
assert.equal(getContext().requestId, 'A');
}),
requestContext.run({ requestId: 'B' }, async () => {
await new Promise(resolve => setTimeout(resolve, 5));
assert.equal(getContext().requestId, 'B');
})
]);
assert.equal(getContext(), undefined);Em testes de integração, envie várias requisições simultâneas com identificadores diferentes e confirme que cada resposta e cada log contém apenas o valor correto. Inclua também erros, timeouts e callbacks de bibliotecas externas.
Erros comuns
- Criar o contexto depois da rota: serviços anteriores ao middleware não enxergam o armazenamento.
- Usar variável global: dados de requisições concorrentes se misturam.
- Chamar getStore() sem fallback: scripts e testes falham fora de uma requisição.
- Guardar objetos grandes: o consumo de memória cresce sem necessidade.
- Usar enterWith() indiscriminadamente: listeners podem herdar um contexto inesperado.
- Confiar no contexto para autenticação: presença de dados não substitui validação de permissão.
- Esperar propagação para workers ou outros processos: o contexto precisa ser enviado explicitamente.
- Ignorar bibliotecas que perdem contexto: logs ficam sem correlação em partes críticas.
Boas práticas para produção
- Crie o contexto no início da requisição.
- Prefira
run()em vez deenterWith(). - Use um contrato tipado e pequeno para o armazenamento.
- Inclua um identificador em respostas e logs.
- Trate a ausência de contexto de maneira explícita.
- Envie contexto mínimo para filas, workers e outros processos.
- Monitore memória, latência e volume de operações assíncronas.
- Teste concorrência, erros e bibliotecas legadas.
- Use
AsyncResourceapenas quando houver perda comprovada. - Revise quais dados chegam ao sistema de logs.
Conclusão
AsyncLocalStorage no Node.js permite propagar metadados por uma cadeia assíncrona sem poluir todas as assinaturas de função. O recurso é especialmente útil para correlacionar logs, rastrear requisições, medir duração e transportar informações pequenas de observabilidade.
Comece com um middleware que crie um requestId, um módulo central para acessar o armazenamento e um logger estruturado. Depois, teste requisições concorrentes e integre filas ou Worker Threads transmitindo apenas o contexto necessário. Com limites claros e cuidado com dados sensíveis, a aplicação ganha rastreabilidade sem aumentar o acoplamento entre as camadas.




