4

Mensageria com BullMQ e Redis: Processamento Assíncrono que Funciona

Um endpoint que envia email, gera PDF e notifica um webhook externo leva 4 segundos para responder. O usuário espera. O load balancer faz timeout. O retry do cliente duplica a operação. Tudo isso porque o processamento acontece de forma síncrona dentro do ciclo de request/response.

BullMQ resolve esse problema com uma abstração de filas sobre Redis que é simples de operar e previsível de debugar. Diferente de soluções como RabbitMQ ou Amazon SQS, BullMQ roda no mesmo ecossistema Node.js, usa Redis como único backing store e oferece retry, prioridade, rate limiting e dead letter queue sem infraestrutura adicional.

Por que BullMQ e não as alternativas

A escolha de mensageria depende de três variáveis: volume de mensagens, complexidade de roteamento e quanto de infraestrutura você quer gerenciar.

CritérioBullMQ + RedisRabbitMQ (AMQP)Amazon SQSKafka
ProtocoloRedis commandsAMQP 0-9-1HTTP/SQS APIKafka protocol
Ordering garantidoPor fila (FIFO)Por filaFIFO opcional (custo extra)Por partição
Retry nativoSim, com backoff configurávelSim, com dead letter exchangeSim, com redrive policyNão nativo (consumer controla)
Rate limitingNativo por filaPluginNão nativoNão nativo
Infra necessáriaRedis (que você já tem)Broker dedicadoConta AWSCluster Kafka + ZooKeeper
Caso idealJobs assíncronos em aplicações Node.js com até ~50k jobs/minRoteamento complexo entre serviços heterogêneosServerless ou integração AWS nativaStreaming de eventos com replay

Se sua aplicação é Node.js, já usa Redis para cache ou sessão, e o volume fica abaixo de 50 mil jobs por minuto: BullMQ é a escolha direta. Acima disso, ou com necessidade de replay de eventos, Kafka entra na conversa. Para roteamento fan-out complexo entre serviços em linguagens diferentes, RabbitMQ faz mais sentido.

Setup: Redis, BullMQ e a primeira fila

Instale as dependências. BullMQ precisa de Redis 5.0+ (usa Streams internamente) e Node.js 18+.

npm install bullmq ioredis
npm install -D @types/node typescript

A conexão com Redis deve ser centralizada. Não crie múltiplas instâncias de IORedis espalhadas pelo código.

// src/lib/redis-connection.ts
import IORedis from "ioredis";

// maxRetriesPerRequest: null é obrigatório para BullMQ.
// O default do ioredis é 20, e BullMQ usa blocking commands (BRPOPLPUSH)
// que não devem ter limite de retry por request.
export const redisConnection = new IORedis(
  process.env.REDIS_URL || "redis://localhost:6379",
  {
    maxRetriesPerRequest: null,
    enableReadyCheck: false,
  }
);

Agora, a primeira fila. O padrão que funciona é separar a definição da fila (producer) do worker (consumer) em módulos distintos. Isso permite escalar workers independentemente do servidor HTTP.

// src/queues/email.queue.ts
import { Queue } from "bullmq";
import { redisConnection } from "../lib/redis-connection";

export interface EmailJobData {
  to: string;
  subject: string;
  templateId: string;
  variables: Record<string, string>;
}

export const emailQueue = new Queue<EmailJobData>("email", {
  connection: redisConnection,
  defaultJobOptions: {
    attempts: 3,
    // Backoff exponencial: 1s, 2s, 4s entre tentativas.
    // Evita sobrecarregar o serviço de email durante instabilidade.
    backoff: {
      type: "exponential",
      delay: 1000,
    },
    // Remove jobs completos após 24h para não inflar a memória do Redis.
    removeOnComplete: { age: 86400 },
    // Mantém jobs falhados por 7 dias para análise.
    removeOnFail: { age: 604800 },
  },
});

Workers: onde o processamento acontece

O worker é um processo separado. Pode rodar no mesmo servidor ou em máquinas dedicadas. BullMQ garante que cada job é processado por exatamente um worker (at-least-once delivery com locking via Redis).

// src/workers/email.worker.ts
import { Worker, Job } from "bullmq";
import { redisConnecti

---

Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/mensageria-bullmq-redis-processamento-assincrono-nodejs](https://www.vivodecodigo.com.br/backend/mensageria-bullmq-redis-processamento-assincrono-nodejs)
Carregando publicação patrocinada...
1