Filas de Mensagens BullMQ e RabbitMQ: Processamento Assíncrono sem Timeouts

Filas de Mensagens BullMQ e RabbitMQ: Processamento Assíncrono sem Timeouts

O Erro Síncrono: Porque a Tua API Paralisa Quando Tenta Fazer Tudo de Uma Vez

Quase todos os programadores cometem este erro nos primeiros projetos: o cliente pede que a loja envie uma fatura detalhada em PDF por email assim que a compra é finalizada.

No código da tua rota em Express ou Fastify, fazes tudo de forma síncrona. Quando o utilizador clica em “Finalizar Encomenda”, o teu servidor arranca um navegador headless via Puppeteer, processa o CSS da fatura, gera o documento vetorial, liga-se ao fornecedor de email e só no final devolve o 200 OK ao utilizador.

Enquanto estás a testar sozinho na tua máquina, o processo demora um segundo e meio. Parece perfeito. Mas no dia em que lanças a campanha de vendas e tens trinta utilizadores a comprar ao mesmo segundo, a tua infraestrutura colapsa.

Trinta instâncias do Chromium arrancam em simultâneo, devoram 6GB de memória RAM e colocam o processador a 100%. O event loop do Node.js bloqueia completamente. O teu servidor Nginx atinge os 60 segundos de tolerância de resposta e devolve um erro brutal: 504 Gateway Timeout. Como o utilizador não percebeu se a compra foi feita, clica mais três vezes e piora ainda mais a fila.

O ERRO SÍNCRONO: BLOQUEIO TOTAL E TIMEOUT 504

  • Browser do Cliente
  • Proxy Nginx API Node.js
  • POST /encomenda
  • POST /encomenda → (Arranca Chromium) (Consome 450MB RAM) (Chama API de Email) (Preso há 15 segundos…)
  • Timeout 60s Atingido — (Event Loop Congelado)
  • 504 Gateway Timeout
  • O cliente vê um erro e volta a clicar três vezes, deitando o servidor abaixo!

Se estás a desenhar web apps de produção, deves seguir uma regra de ouro: nenhum processo de servidor web deve executar operações pesadas de CPU ou chamadas externas demoradas dentro do ciclo imediato de pedido e resposta.

O Modelo Assíncrono: Confirmação Imediata com Filas de Tarefas

A solução correta para este problema é desacoplar a receção do pedido do processamento do trabalho através de um sistema de filas baseado em Redis, como o BullMQ, ou um broker de mensagens como o RabbitMQ.

Quando o cliente clica para comprar, o teu servidor faz apenas três ações ultrarrápidas:

  1. Valida os dados da encomenda contra o teu esquema JSON.
  2. Guarda a encomenda na base de dados com o estado pendente.
  3. Adiciona uma pequena mensagem à fila ({ orderId: 4502, email: "cliente@empresa.pt" }).

A tua API responde imediatamente com o código HTTP 202 Accepted e entrega um identificador de trabalho (jobId). Toda esta operação demora apenas 10 milissegundos. A seguir, processos dedicados (workers) em segundo plano vão buscar as tarefas à fila ao ritmo que o servidor aguenta, sem nunca sobrecarregar o hardware.

ARQUITETURA ASSÍNCRONA: RESPOSTA RÁPIDA + POOL WORKERS

  • Browser
  • API de Entrada (Nginx/Node) Fila Redis (BullMQ) Processos Workers
  • POST /encomenda
  • PUSH Tarefa {orderId}
  • 202 Accepted (10ms)
  • (Browser aguarda via SSE ou consulta periódica)
  • WORKER 1 Processa → (Gera PDF)
  • WORKER 2 Processa → (Envia Email)
  • Trabalho Concluído

Se receberes cem compras no mesmo minuto, as mensagens ficam tranquilamente guardadas na memória do Redis, enquanto os teus workers geram duas faturas de cada vez sem gastar mais memória do que a disponível. É esta arquitetura que aplicamos na criação de websites de alta performance.

Como Implementar BullMQ com Redis e Backoff Exponencial em Node.js

Vejamos como podes estruturar este fluxo em Node.js com BullMQ. O BullMQ oferece gestão automática de concorrência, atrasos inteligentes e retentativas exponenciais.

No teu ficheiro de criação de filas, defines os parâmetros de tolerância:

// src/queues/faturaQueue.ts
import { Queue } from 'bullmq';

const conexaoRedis = { host: '127.0.0.1', port: 6379 };

export const faturaQueue = new Queue('faturas', {
  connection: conexaoRedis,
  defaultJobOptions: {
    attempts: 5,
    backoff: {
      type: 'exponential',
      delay: 4000, // Tenta aos 4s, 8s, 16s, 32s e 64s
    },
    removeOnComplete: { count: 1000 },
    removeOnFail: false, // Mantém falhas para auditoria
  },
});

// Dentro da tua rota HTTP:
app.post('/api/checkout', async (req, res) => {
  const { encomendaId, email } = req.body;
  
  // Envia para a fila e liberta o cliente de imediato
  const trabalho = await faturaQueue.add('gerarFatura', {
    encomendaId,
    email,
    criadoEm: new Date().toISOString(),
  });

  return res.status(202).json({
    estado: 'aceite',
    trabalhoId: trabalho.id,
    mensagem: 'A tua fatura está a ser gerada em segundo plano.',
  });
});

De seguida, crias o processo de trabalho (worker) com limite rigoroso de concorrência para proteger a memória RAM da tua máquina:

// src/workers/faturaWorker.ts
import { Worker, Job } from 'bullmq';
import { criarDocumentoPdf } from '../servicos/pdfService.js';
import { enviarEmailComAnexo } from '../servicos/emailService.js';

const conexaoRedis = { host: '127.0.0.1', port: 6379 };

export const faturaWorker = new Worker(
  'faturas',
  async (job: Job) => {
    job.updateProgress(10);
    console.log(`[Worker] A processar fatura para a encomenda #${job.data.encomendaId}`);

    // Fase 1: Geração pesada do PDF
    const bufferPdf = await criarDocumentoPdf(job.data.encomendaId);
    job.updateProgress(60);

    // Fase 2: Envio através do servidor SMTP
    await enviarEmailComAnexo({
      destinatario: job.data.email,
      assunto: `Fatura da tua encomenda #${job.data.encomendaId}`,
      ficheiro: bufferPdf,
    });

    job.updateProgress(100);
    return { sucesso: true };
  },
  {
    connection: conexaoRedis,
    concurrency: 2, // Limite de 2 tarefas em simultâneo por núcleo de CPU
  }
);

faturaWorker.on('failed', (job, err) => {
  console.error(`[Alerta] Trabalho ${job?.id} falhou na tentativa ${job?.attemptsMade}: ${err.message}`);
});

Ao organizares estes processos em contentores com Docker, podes escalar a capacidade de processamento sem alterar uma única linha da tua aplicação web.

Dead-Letter Queues e Mensagens Envenenadas: Como Isolar Falhas

Imagina que um utilizador introduz um email inválido como cliente@@dominio..pt ou que o servidor de email rejeita a tua mensagem. Sem proteções adequadas, o teu trabalhador vai tentar reenviar o email infinitamente, gastando CPU e poluindo os teus ficheiros de registo com erros repetidos. Isto chama-se uma mensagem envenenada (poison pill).

Para resolveres este problema, as arquiteturas de filas usam Dead-Letter Queues (DLQ). Quando uma tarefa falha a sua última tentativa permitida (por exemplo, na 5ª tentativa), o motor da fila retira-a da circulação ativa e guarda-a numa lista de quarentena.

DEAD-LETTER QUEUE (DLQ) E ISOLAMENTO DE FALHAS

  • Fila Ativa: [Tentativa 1: Falhou] → Espera 4s
  • Tentativa 2: Falhou: Espera 8s
  • Tentativa 3: Falhou: Espera 16s
  • (Esgotou todas as tentativas)
  • DEAD-LETTER QUEUE (Tarefas em Quarentena)
  • Guarda os parâmetros exatos do pedido
  • Regista o erro completo e a stack trace
  • Emite um alerta para o administrador
  • (Após correção do problema)
  • Administrador reenvia a tarefa com um só clique

Através de ferramentas visuais como o Bull-Board disponíveis no GitHub, a tua equipa pode inspecionar exatamente o motivo da falha, corrigir o email do cliente na base de dados e reenviar a tarefa sem necessidade de recorrer a novos deploys.

Boas Práticas de Engenharia: Idempotência, Memória Redis e Monitorização

Para garantires que o teu sistema de filas nunca falha em ambientes de grande escala, segue estas três regras essenciais de infraestrutura:

  1. Usa Chaves de Idempotência: Se ocorrer uma micro-falha de rede entre o teu worker e o Redis exatamente após o envio do email, o broker pode achar que o processo caiu e voltar a agendar o trabalho. Se atribuíres um jobId determinístico (por exemplo fatura-encomenda-4502), o BullMQ ignora automaticamente qualquer duplicação.
  2. Separa o Redis de Cache do Redis de Filas: No Redis partilhado, as políticas de libertação de memória (maxmemory-policy: allkeys-lru) podem apagar dados de filas em segundo plano quando a cache enche. Mantém sempre uma instância de Redis dedicada exclusivamente às tuas filas de processamento.
  3. Monitoriza a Profundidade da Fila (Lag): Configura alertas para quando a quantidade de tarefas em espera ultrapassar os 150 elementos durante mais de 3 minutos. Se isso acontecer, significa que precisas de arrancar mais instâncias de workers.

Se precisas de rever a arquitetura dos teus servidores ou desenhar soluções distribuídas à prova de falhas, a nossa equipa de consultoria técnica apoia o desenho e implementação de infraestruturas inspiradas nos padrões da WebXtek.

Na prática, aplicas exatamente estes padrões operacionais em setores exigentes como saas empresarial b2b e especialistas em devops e arquitetura cloud. Quando alinhas a tua estrutura através de web apps e websites de alta performance, transformas boas práticas técnicas numa vantagem competitiva real e mensurável para o teu negócio.

[+] [ INDEX_DIRECTORY ] 13 tópicos ▼
[ SYSTEM.FAQ ]

Perguntas Frequentes

Porque é que gerar faturas em PDF dentro do pedido HTTP causa erros 504 no servidor?

Iniciar motores de renderização como o Chromium ou bibliotecas gráficas pesadas gasta centenas de megabytes de RAM e demora vários segundos. Se dezenas de utilizadores fizerem encomendas ao mesmo tempo, o proxy reverso (como o Nginx) atinge o tempo limite e devolve um 504 Gateway Timeout.

Qual é a diferença fundamental entre o BullMQ e o RabbitMQ?

O BullMQ é uma biblioteca ultrarrápida baseada em Redis feita para Node.js, perfeita para trabalhos com retentativas, atrasos programados e controlo de concorrência. O RabbitMQ é um broker empresarial baseado em AMQP, desenhado para topologias complexas de encaminhamento e múltiplos sistemas heterogéneos.

O que é uma Dead-Letter Queue (DLQ) e para que serve?

Uma Dead-Letter Queue é uma fila de quarentena para onde são enviados os trabalhos que falharam consecutivamente após esgotarem todas as tentativas. Isolar mensagens corrompidas evita que os teus processos de trabalho caiam em ciclo e permite analisar o erro sem bloquear a fila principal.

Como é que o teu frontend sabe que o ficheiro PDF ficou pronto?

A tua API responde imediatamente com o código HTTP 202 Accepted e devolve o identificador do trabalho (jobId). O teu frontend faz pedidos periódicos a um endpoint de estado (/api/jobs/:id) ou escuta o evento através de WebSockets ou Server-Sent Events (SSE).

> INICIAR_PROJETO

Precisa de um website que transmita confiança, apareça na pesquisa e dê mais força à sua presença digital? Comece a conversa aqui.