9 min de leitura
node.js
background jobs
bull
postgresql
arquitetura

Background Jobs em Node.js: BullMQ vs pgmq vs Construir Próprio

Comparação técnica entre BullMQ, pgmq e solução proprietária para jobs assincronos em Node.js. Trade-offs reais em latência, escala e complexidade operacional.

Background Jobs em Node.js: BullMQ vs pgmq vs Construir Próprio

TL;DR

  • BullMQ é a escolha mais segura para startups: oferece retry automático, DLQ (dead letter queue), e observabilidade sem custo operacional (Redis gerido é ~€20/mês).
  • pgmq elimina uma dependência externa (Redis) se já usas PostgreSQL, mas adiciona polling CPU e é ~30% mais lento em cenários de alta concorrência (>500 jobs/s).
  • Construir job queue própria faz sentido em casos muito específicos: fintech com requisitos de auditoria extrema, processamento crítico com SLA <100ms p99, ou quando tens eng sénior disponível com banda para manutenção contínua.
  • A gotcha mais comum: assumir que "está em cache, é rápido" sem medir latência p99 realmente, muitos sistemas de jobs custom sofrem de stalls quando backup ou lock contention acontece.
  • Para 90% dos SaaS B2B, BullMQ + Redis gerido é o sweet spot: custo/complexidade minimizado, comunidade grande, e deixa-te focar no negócio.

Porquê Este Problema Importa em 2024

Se estás a construir qualquer aplicação Node.js em produção, em algum ponto vais precisar processar trabalho de forma assincronona: enviar emails, gerar PDFs, integrar com APIs externas, processar webhooks, agregar dados. Deixar isso numa fila é crítico para manter latência HTTP baixa e oferecer experiência responsiva ao utilizador.

A escolha da ferramenta de background jobs é deceptivamente importante. Uma má decisão aqui multiplica-se rapidamente: pode impedir escalabilidade, criar pontos de falha únicos, aumentar complexidade operacional em 10x, ou deixar-te preso a tecnologia que não faz sense para o que realmente precisas.

Em Portugal, vemos muitos SaaS construírem soluções proprietárias desnecessárias (tabelas em PostgreSQL com polling, filas em memória que perdem trabalho em restart) ou, no outro extremo, usarem Celery + RabbitMQ num projeto Node.js (desperdício de complexity).

Este artigo foca a realidade prática: quais são os números reais, onde é que cada ferramenta falha em produção, e quando é que realmente vale a pena construir próprio.


BullMQ: A Escolha Pragmática para 95% dos Casos

BullMQ é a library de jobs queue mais madura no ecossistema Node.js moderno. Construída em torno de Redis, oferece semantics sólidas: retry automático, exponential backoff, dead letter queues, scheduling, rate limiting, e observabilidade integrada.

Números reais:

  • Throughput: ~1000-3000 jobs/s por worker (tipicamente 1-4 workers por servidor), dependendo da complexidade do job.
  • Latência (p50): 5-20ms entre enqueue e pickup.
  • Latência (p99): 50-150ms em carga normal (cluster Redis com persistence).
  • Overhead por job: ~500 bytes em Redis (metadados + retry info).
  • Custo mensal: €20-50 para Redis gerido (Upstash, Redis Cloud) em SaaS típico com 10M jobs/mês.

Vantagens concretas:

  1. Retry automático com backoff exponencial: Se um job falha, BullMQ tenta novamente em 2s, depois 4s, depois 8s, até 60s. Isto resolve 70% dos problemas transientes (API timeout, rate limiting, falhas de rede).

  2. Observabilidade sem instrumentação manual: BullMQ expõe eventos (processing, completed, failed, delayed). Podes conectar isto a qualquer observabilidade tool (DataDog, Sentry, Grafana) em 30 minutos.

  3. Durabilidade: Redis (especialmente com persistence ativado) garante que jobs não se perdem entre restarts, ao contrário de filas em memória.

  4. Scheduled jobs: Agendar tarefas para tempos específicos é trivial (queue.add(data, { delay: 3600000 }) para 1 hora depois).

  5. Rate limiting por segundo: Podes limitar jobs a, digamos, 100/s para não sobrecarregar uma API externa.

Armadilhas em produção:

A maior falha que vejo: assumir que Redis é "sempre rápido". Não é. Se o teu cluster Redis está sobrecarregado (e.g., concorrência no backend está a encher o Redis enquanto workers estão a ler), latência p99 pode saltar para 500ms-1s. Isto cria backlog invisível.

Mitigação: monitoriza connected_clients, used_memory, e latência de ping Redis. Se Redis está a rejeitar conexões, já perdeste.

Segunda gotcha: esquecer que await queue.process() bloqueia forever. Se tiveres múltiplos workers mas não escalar número de instâncias conforme volume cresce, vai haver um ponto em que workers esmagam o servidor (CPU em 100%, memória a crescer).

Quando usar BullMQ:

  • Qualquer SaaS B2B com volume <10M jobs/mês.
  • Quando precisas de retry automático + observabilidade sem esforço.
  • Se Redis gerido não é um "não" categórico pela tua company policy.

pgmq: Eliminar Redis como Dependência

pgmq é a alternativa crescente. É uma Postgres extension que implementa job queue diretamente na BD, sem Redis. A premissa é: se já usas PostgreSQL (e quase todos usam), porquê adicionar outra ferramenta de infraestrutura?

Números reais:

  • Throughput: ~300-800 jobs/s por worker (5-10x mais lento que BullMQ em push throughput).
  • Latência (p50): 20-50ms (polling overhead).
  • Latência (p99): 200-500ms em polling (depende de poll_interval).
  • Overhead por job: ~2KB por linha em PostgreSQL.
  • Custo mensal: Zero operacional extra (já pagas Postgres). Mas CPU Postgres +15-20% se jobs são volume alto.

Vantagens concretas:

  1. Uma ferramenta menos: Não precisas gerenciar Redis como infraestrutura separada. Simplifica deploy, backups, monitoring.

  2. Transacções ACID completas: Podes enqueue um job e atualizar business logic na mesma transacção. Impossível com Redis.

BEGIN;
  UPDATE users SET balance = balance, 100 WHERE id = $1;
  INSERT INTO pgmq.queue ('payment_jobs') VALUES (jsonb_build_object('user_id', $1, 'amount', 100));
COMMIT;

Se o UPDATE falha, o job nunca entra na queue. Isto é powerful para fintech ou domínios onde auditoria é crítica.

  1. Sem extra operational burden: Backups de Postgres já cobrem tudo, incluindo jobs não processados.

Armadilhas em produção:

A maior: pgmq usa polling por default. Isto significa que um worker periodicamente (normalmente a cada 1-5 segundos) faz query a SELECT * FROM pgmq.queue WHERE consumed=false LIMIT 1. Isto é ineficiente e cria latência variável alta.

Se tens 100 workers polleando a cada 1s, são 100 queries desnecessárias por segundo na tua BD.

Segunda gotcha: contention em PostgreSQL. Se vários workers fazem UPDATE ao mesmo tempo na mesma row (para marcar job como processing), PostgreSQL precisa de serializar isto. Com >10 workers processeando concorrentemente, começas a ver lock timeouts.

Terceira: pgmq foi criada recentemente (2023-2024). Comunidade é pequena. Se encontras edge case, não há muita gente com experiência.

Quando usar pgmq:

  • Volume de jobs é baixo (<1M/mês), e latência p99 de 500ms é aceitável.
  • Precisas de transacções ACID entre job enqueueing e business logic (fintech, domínios regulados).
  • A tua company tem política explícita: "sem Redis em produção".
  • Estás confortável a ser early adopter.

Construir Próprio: Quando Realmente Faz Sentido

Isto é o cenário mais perigoso. Construir queue proprietária em Node.js é possível (tabela em PostgreSQL + polling, ou em-memória com persistência em ficheiro), mas deve-se fazer apenas em circunstâncias muito específicas.

Custos reais de construir próprio:

  • Esforço inicial: 40-80 horas de eng sénior (ou 200+ horas de eng júnior).
  • Manutenção: 10-20 horas/mês investigando falhas edge case (stalls sob carga, corrupção de estado se crash na altura errada).
  • Testing: complexo. Precisas de teste caótica (failure injection, network partition simulation) para ter confiança.
  • Observabilidade: tens de instrumentar tudo manualmente.
  • Escala: a maioria das soluções próprias que vi começam a falhar em >100 jobs/s sob carga sustentada.

Casos onde construir próprio é defensável:

  1. Requisitos de auditoria extrema em fintech: Se precisas de log imutável de cada tentativa de job, com chain de assinatura criptográfica, ou timing preciso para compliance regulatória, e nenhuma ferramenta off-the-shelf o oferece.

  2. Latência <100ms p99 é hard requirement: Por exemplo, sistema de detecção de fraude em tempo real que precisa feedback <50ms. BullMQ e pgmq podem não garantir isto sob carga.

  3. SLA de uptime 99.99%+ com multi-region: Queres replicação ativa-ativa de jobs entre regiões com failover automático. Standard job queues não oferecem isto.

  4. Integração profunda com domínio específico: Por exemplo, em jogos online, queue de ações de jogadores precisa de ordering garantida por sessão, com rollback de estado. Isto é custom suficiente para justificar solução própria.

  5. Tens eng sénior (5+ anos) com banda protegida: Não é "quando temos um dev livre". É quando tens um architect dedicado a isto.

O que fiz bem em projeto próprio que viu produção:

Há 3-4 anos trabalhámos em plataforma de processamento de ficheiros para cliente no retail. Volume era 50K jobs/dia, com SLA de 30 minutos. Construímos queue em PostgreSQL com modelo de ocupação específico:

  • Jobs eram imutáveis após enqueue (append-only log).
  • Worker process fazia lease exclusivo por 5 minutos (com heartbeat).
  • Se worker morria, job voltava à queue automaticamente.
  • Retry logic era explícita: job podia ter até 3 tentativas, depois ia para DLQ para revisão manual.

Isto funcionou bem porque era específico ao problema (ficheiros grandes, processamento de longa duração, integração com storage S3), e o custo de errar era alto (dados de clientes em risco).

Mas o esforço foi 60 horas iniciais + 5 horas/mês de manutenção. Se tivéssemos usado BullMQ, seria 5 horas iniciais + 0 horas/mês.


Comparação Técnica Directa

AspectoBullMQpgmqPróprio
Throughput (jobs/s)1000-3000300-800Varia (100-2000)
Latência p99 (ms)50-150200-500100-1000 (instável)
Setup time30 min15 min40-80 horas
Manutenção (h/mês)1-22-310-20
Infraestrutura extraRedis gerido (€20-50)NenhumaNenhuma (mas CPU Postgres +15%)
Retry automáticoSim (built-in)Sim (built-in)Tens de implementar
ObservabilidadeExcelenteBásicaTens de implementar
DurabilidadeRedis persistencePostgreSQL ACIDDepende implementação
Scaling horizontalTrivial (mais workers)Contention à medida que cresceDifícil (lock contention)
Community sizeGrande (2K+ stars, ativo)Pequeno (crescente)Nenhuma
Risco técnicoBaixoMédioAlto

Recomendação Pragmática por Cenário

SaaS B2B típico (volume <5M jobs/mês, latência p99 <500ms aceitável):

Usa BullMQ. Não penses muito. O custo operacional é trivial, comunidade é massive, e deixa-te focar em funcionalidade.

Fintech, compliance-heavy, ou auditoria crítica:

pgmq se volume é baixo e ACID guarantee é crucial. Se volume é alto, considera BullMQ + audit log separado (tabela em PostgreSQL onde registas cada job enqueued + outcome).

Processamento em tempo real (recomendação, fraud, pricing dinâmico):

BullMQ com workers distribuídos e SLA monitorizado. Se precisas <50ms p99 garantido, pode ser que nenhuma solução standard funcione e precises de custom solution. Mas isto é raro.

Legacy codebase que já usa Celery, RabbitMQ, ou SQS:

Migra para BullMQ quando tiveres ciclo de refactor. O payoff em redução de operacional complexity é enorme.


O que Realmente Importa

A decisão real não é qual ferramenta é "melhor" tecnicamente. É:

  1. Qual é o teu burn rate em eng sénior? Se cada hora custa €150, e construir próprio custa 100 horas, são €15K. BullMQ custa €300/ano. Matemática é clara.

  2. Qual é o custo de downtime em job queue? Se jobs perdem-se e ninguém reclama (e.g., notificações via email que users não veem), talvez reliability lower é aceitável. Se é processamento de pagamentos, absolutamente não.

  3. Quanto vais gastar em troubleshooting quando algo falha em produção? Se escolhes tool obscura e falha, podes passar 20 horas investigando. Ferramenta popular? Há 100 pessoas com problema idêntico no GitHub.


Conclusão

Para a maioria dos SaaS B2B em Portugal, a resposta é BullMQ. Custo operacional mínimo, comunidade grande, reliability provada, e deixa-te focar no que importa: produto.

pgmq é alternativa válida se queres eliminar Redis como dependência e Volume é baixo. Construir próprio é armadilha sedutor que vista em retrospectiva custa muito engineering time.

Se estás a enfrentar um problema parecido (scaling background jobs, ou questionando tech stack para processamento assincronao), marca uma conversa em https://impact-origin.com/agendamento.

Impact OriginGostaste deste artigo?Na Impact Origin ajudamos fundadores e equipas a construir e escalar software à medida, do MVP da startup ao próximo passo. Se tens um projeto em mente, vamos falar.