RabbitMQ com Node.js permite desacoplar partes de uma aplicação usando filas e exchanges. Em vez de uma requisição HTTP esperar que todo o processamento termine, o produtor publica uma mensagem e um ou mais consumidores executam o trabalho de forma independente. Essa arquitetura ajuda em envio de e-mails, geração de relatórios, processamento de imagens, integrações e tarefas que precisam sobreviver a falhas temporárias.
Uma fila não garante confiabilidade sozinha. É necessário configurar durabilidade, confirmação de publicação, acknowledgement manual, retries controlados, dead-letter, idempotência e observabilidade. Neste guia, você verá como construir esse fluxo com Node.js. Para revisar a base da plataforma, leia o que é Node.js e como criar uma API com Node.js.
Como o RabbitMQ organiza mensagens?
Produtores publicam mensagens em exchanges. A exchange encaminha cada mensagem para uma ou mais filas de acordo com bindings e routing keys. Consumidores recebem mensagens das filas. Esse modelo evita que o produtor precise conhecer cada consumidor e facilita adicionar novos fluxos sem alterar o código que cria o evento.
Os tipos mais comuns de exchange são direct, topic, fanout e headers. Uma exchange direct usa correspondência exata da routing key. Topic aceita padrões como orders.*. Fanout distribui para todas as filas vinculadas. Escolha o tipo a partir do contrato de roteamento, não apenas do exemplo mais simples.
Instalação
npm init -y
npm install amqplib
npm install --save-dev node:testConfigure a URL de conexão por variável de ambiente e use TLS quando o broker estiver fora de uma rede privada controlada. Não coloque credenciais no repositório.
Declarando exchange e fila
import amqp from 'amqplib';
const connection = await amqp.connect(process.env.AMQP_URL);
const channel = await connection.createConfirmChannel();
await channel.assertExchange('orders.events', 'topic', {
durable: true
});
await channel.assertQueue('billing.order-created', {
durable: true,
deadLetterExchange: 'orders.dead'
});
await channel.bindQueue(
'billing.order-created',
'orders.events',
'order.created'
);Declarar recursos no início torna o serviço autocontido, mas várias equipes devem coordenar alterações incompatíveis. Em ambientes maiores, a topologia pode ser gerenciada por infraestrutura como código.
Publicando com confirmação
Mensagens persistentes e filas duráveis aumentam a chance de sobrevivência a reinícios, mas o produtor também precisa confirmar que o broker aceitou a publicação.
export async function publishOrderCreated(order) {
const payload = Buffer.from(JSON.stringify({
eventId: crypto.randomUUID(),
eventType: 'order.created',
occurredAt: new Date().toISOString(),
data: {
orderId: order.id,
customerId: order.customerId,
total: order.total
}
}));
channel.publish(
'orders.events',
'order.created',
payload,
{
persistent: true,
contentType: 'application/json',
messageId: crypto.randomUUID(),
timestamp: Date.now()
}
);
await channel.waitForConfirms();
}Publisher confirms informam que o broker assumiu responsabilidade pela mensagem. Eles não significam que um consumidor processou o evento. Para operações importantes, registre o estado de publicação e considere o padrão transactional outbox para alinhar banco e mensageria.
Consumidor com ack manual
await channel.prefetch(10);
await channel.consume(
'billing.order-created',
async message => {
if (!message) return;
try {
const event = JSON.parse(message.content.toString('utf8'));
validateOrderCreated(event);
await processBillingOnce(event);
channel.ack(message);
} catch (error) {
if (isTemporary(error)) {
channel.nack(message, false, true);
} else {
channel.nack(message, false, false);
}
}
},
{ noAck: false }
);O ack deve ser enviado somente depois que todos os efeitos necessários foram concluídos. Se o processo morrer antes do ack, o RabbitMQ pode entregar a mensagem novamente. Por isso, o consumidor precisa ser idempotente.
Prefetch e concorrência
prefetch limita quantas mensagens não confirmadas podem ficar com o consumidor. Um valor alto aumenta paralelismo, mas pode concentrar trabalho, memória e conexões em uma única instância. Meça duração, CPU, banco e APIs externas antes de escolher o valor.
Não abra um canal por mensagem. Mantenha conexões longas, use canais com responsabilidade clara e recrie-os quando a conexão cair. O reconnect precisa de backoff com jitter, como explicado em retry com backoff no Node.js.
Retries sem loop infinito
Reenfileirar imediatamente uma mensagem problemática pode criar um loop que consome toda a fila. Prefira filas de retry com TTL e dead-letter exchange. Cada tentativa incrementa um contador confiável; após o limite, a mensagem vai para uma dead-letter queue para análise.
- classifique falhas temporárias e permanentes;
- defina número máximo de tentativas;
- use atrasos crescentes;
- preserve eventId e correlationId;
- não altere o payload silenciosamente;
- crie procedimento para reprocessamento seguro.
Idempotência
Entregas duplicadas são esperadas em sistemas at-least-once. Guarde o eventId processado em uma tabela com restrição única ou faça a própria operação de negócio ser idempotente. O registro e o efeito devem ocorrer na mesma transação quando possível.
async function processBillingOnce(event) {
await database.transaction(async tx => {
const inserted = await tx.tryInsertProcessedEvent(event.eventId);
if (!inserted) return;
await tx.createInvoice({
orderId: event.data.orderId,
total: event.data.total
});
});
}Contratos de mensagem
Defina versão, tipo, identificador, horário e payload. Valide mensagens em runtime; tipos TypeScript não protegem dados vindos da rede. Consulte Zod no TypeScript para criar schemas executáveis.
Evite incluir dados pessoais desnecessários. Mensagens podem permanecer no broker, em backups e em dead-letter queues por mais tempo que uma requisição HTTP.
Segurança
- use usuários separados por serviço;
- conceda permissões apenas às exchanges e filas necessárias;
- ative TLS e valide certificados;
- rotacione credenciais;
- limite tamanho máximo das mensagens;
- não use desserialização insegura;
- proteja a interface administrativa;
- monitore publicação e consumo anormais.
Observabilidade
Acompanhe profundidade da fila, taxa de publicação, taxa de entrega, mensagens não confirmadas, redeliveries, idade da mensagem mais antiga, tempo de processamento e conteúdo da dead-letter queue. Logs devem incluir eventId, messageId, routing key, tentativa e requestId original.
Não use eventId como label de métrica. Ele serve para logs e traces. Métricas precisam de dimensões limitadas para não aumentar a cardinalidade.
Graceful shutdown
Ao receber SIGTERM, pare de aceitar novas requisições, cancele o consumidor, aguarde tarefas em andamento, envie os acks e feche canal e conexão. Defina um prazo máximo para que o orquestrador não mate o processo no meio da confirmação.
process.on('SIGTERM', async () => {
await channel.cancel(consumerTag);
await waitForInFlightJobs({ timeoutMs: 20_000 });
await channel.close();
await connection.close();
process.exit(0);
});Testes
Teste validação de payload, duplicidade, falha temporária, falha permanente, retry, dead-letter e encerramento. Em integração, use um broker isolado e nomes de filas únicos por suíte. Confirme que uma mensagem não recebe ack quando a transação falha.
Os princípios de testes unitários com Jest ajudam a separar regras de negócio da infraestrutura.
Erros comuns
- Usar auto-ack: a mensagem pode ser perdida antes do processamento;
- Requeue infinito: uma mensagem inválida bloqueia capacidade continuamente;
- Ignorar duplicidade: efeitos como cobrança podem acontecer duas vezes;
- Publicar sem confirmação: o produtor pode assumir sucesso antes do broker;
- Mensagens gigantes: memória e rede são consumidas de forma desnecessária;
- Sem dead-letter: falhas permanentes desaparecem no fluxo operacional.
Checklist de produção
- exchange e filas são duráveis;
- publicações usam confirmação;
- consumidores usam ack manual;
- prefetch foi medido;
- retries têm atraso e limite;
- dead-letter queue é monitorada;
- operações são idempotentes;
- shutdown foi testado.
Referências oficiais
Conclusão
RabbitMQ com Node.js oferece uma base sólida para trabalho assíncrono, desde que a aplicação trate as garantias reais do sistema. Publisher confirms, ack manual, prefetch, retry com atraso, dead-letter e idempotência são partes do mesmo desenho.
Comece com um contrato simples e observável. Depois teste quedas do consumidor, indisponibilidade do broker e mensagens duplicadas. Uma fila confiável não é aquela que nunca falha, mas aquela que preserva o trabalho, torna falhas visíveis e permite recuperação segura.




