O time precisa enviar e-mail de confirmação, gerar PDF e sincronizar com um ERP depois que o pedido é gravado. A decisão não é “qual fila é mais rápida”, e sim: o job pode nascer na mesma transação que o pedido, ou pode ser perdido e reenfileirado depois? Quase toda escolha entre uma fila em Postgres e o BullMQ sobre Redis se resolve respondendo isso primeiro.
O que cada opção garante de verdade
Uma fila em Postgres, seja uma tabela própria com SELECT ... FOR UPDATE SKIP LOCKED ou uma biblioteca como pg-boss ou Graphile Worker, grava o job no mesmo banco dos dados de negócio. Se a transação do pedido falha, o job some junto. Se ela confirma, o job existe. Não há janela em que o pedido foi salvo e o job não.
O BullMQ guarda tudo no Redis. A garantia de entrega é boa e a ergonomia é superior (prioridades, delays, jobs repetíveis, rate limit por fila, painel pronto), mas a escrita no Redis acontece fora da transação do banco. Em algum momento o código vai gravar o pedido e, se o processo morrer antes do queue.add, o job se perde. Quem usa BullMQ com seriedade acaba implementando um outbox: grava o evento numa tabela na transação e um relay publica no Redis. Nesse ponto o Postgres já faz parte da solução.
SKIP LOCKED. Você elimina uma peça móvel e o relay.Onde o Postgres começa a sofrer
Fila em tabela gera muito churn: insere, atualiza estado, apaga ou arquiva. Isso produz tuplas mortas, e o autovacuum precisa acompanhar. Em volume de centenas de jobs por segundo sustentado, com workers fazendo polling agressivo, a tabela incha, os índices degradam e a fila passa a competir por I/O com as consultas do produto. O sinal clássico é a latência de consultas não relacionadas subir no horário de pico de processamento.
Há mitigações reais: particionar a tabela por dia e descartar partições antigas em vez de dar DELETE, usar LISTEN/NOTIFY para acordar workers em vez de polling curto, manter o índice parcial só sobre jobs pendentes. Elas funcionam, mas exigem alguém que entenda de manutenção de Postgres. Se esse alguém não existe no time, o custo é maior do que parece.
Como ordem de grandeza, a maioria dos sistemas de gestão e SaaS B2B de porte médio fica folgada com dezenas de jobs por segundo. Esse volume cabe em Postgres sem cerimônia. Quando o gargalo aparece, normalmente é em jobs de longa duração segurando conexão, e não na taxa de enfileiramento.
Onde o Redis com BullMQ vence
Três cenários justificam a peça extra. O primeiro é volume alto e jobs curtos, em que a vida útil de cada job é de milissegundos e o custo de gravar e apagar em disco transacional é desperdício. O segundo é a necessidade de recursos de agendamento sofisticados: limite de concorrência por grupo, prioridade dinâmica, jobs repetíveis com cron e deduplicação por ID. Dá para construir tudo isso em SQL, mas você estará reescrevendo o BullMQ pior. O terceiro é isolamento de carga: o processamento pesado não deve encostar no banco principal de forma alguma.
O custo é operacional. Redis precisa de persistência configurada (AOF, de preferência com appendfsync everysec), política de memória em noeviction para que ele recuse escrita em vez de descartar jobs silenciosamente, e monitoramento de memória. Redis gerenciado resolve parte, e cobra por isso. Um erro comum é rodar BullMQ no mesmo Redis usado como cache com política allkeys-lru: sob pressão de memória, o Redis despeja chaves de jobs e a fila perde trabalho sem erro algum.
Retry, idempotência e o que ninguém testa
Em ambas as opções, a entrega é pelo menos uma vez. O job vai rodar duas vezes em algum momento: worker que morre depois de chamar a API do ERP e antes de confirmar, timeout que expira com o trabalho ainda em curso. A decisão de arquitetura que importa mais do que a fila é tornar o handler idempotente, com chave de idempotência enviada ao sistema externo ou checagem de estado antes do efeito colateral.
Configure também o backoff exponencial com teto e um limite de tentativas. Sem isso, um endpoint externo fora do ar transforma a fila num gerador de carga contra o serviço que já está caído. E mantenha uma dead letter visível: um job que falhou cinco vezes precisa aparecer num painel ou alerta, não em log que ninguém lê.
Observabilidade: o que medir desde o primeiro dia
Uma fila sem métricas é uma caixa-preta que só se manifesta quando o cliente reclama. Quatro números bastam para começar: idade do job mais antigo pendente, tamanho da fila por tipo, taxa de falha por handler e duração p95 de execução. A idade do mais antigo vale mais que o tamanho: mil jobs pendentes com o mais velho de dois segundos é um sistema saudável, e dez jobs com o mais velho de uma hora é um incêndio.
Em Postgres, essas consultas são um SELECT contra a própria tabela e entram em qualquer dashboard. Em BullMQ, o próprio ecossistema expõe contadores por estado, e vale exportá-los para o sistema de métricas que o time já usa em vez de depender só do painel visual. Em ambos os casos, o alerta que mais salva é o de “nenhum job concluído nos últimos N minutos com fila não vazia”, porque pega worker travado, deploy quebrado e credencial expirada de uma vez.
Migrar de uma para outra sem parar o sistema
A troca, nos dois sentidos, é menos dramática se o código de negócio nunca falar direto com a biblioteca da fila. Uma interface fina, algo como enqueue(tipo, payload, opcoes), isola a decisão. Com ela, a migração segue um roteiro previsível: passa a gravar nas duas filas atrás de uma flag, deixa os workers antigos drenarem o que já estava pendente, confirma que a fila antiga zerou e só então remove o caminho velho. Evite o corte seco, porque o job em voo no momento da troca é exatamente o que some.
Um cuidado específico ao sair do Postgres para o Redis: jobs agendados para o futuro precisam ser reenfileirados com o atraso restante, calculado a partir do horário original, e não com o atraso inicial. Ao voltar do Redis para o Postgres, o risco é inverso, e o payload costuma carregar tipos que o BullMQ serializava de forma implícita, como datas. Teste a desserialização de jobs reais antes do corte.
Tabela de decisão
| Critério | Postgres (SKIP LOCKED) | BullMQ + Redis |
|---|---|---|
| Atomicidade com dado de negócio | Nativa, mesma transação | Exige outbox |
| Volume sustentado alto | Degrada, exige manutenção | Confortável |
| Recursos de agendamento | Básicos ou via biblioteca | Completos e maduros |
| Peças operacionais | Nenhuma além do banco | Redis com persistência |
| Risco principal | Inchaço da tabela | Perda por falha de configuração |
O que escolher
Um último critério, frequentemente subestimado: a experiência de quem depura. Com a fila em Postgres, o suporte consulta o job com um SELECT e cruza com o pedido por JOIN, na mesma ferramenta de sempre. Com Redis, é preciso outro cliente, outro vocabulário e outro acesso em produção. Para times pequenos, esse atrito diário pesa mais que qualquer diferença de throughput.
Para uma aplicação que ainda não tem Redis em produção e processa menos de algumas dezenas de jobs por segundo, Postgres com pg-boss ou Graphile Worker. Você ganha atomicidade, um componente a menos para monitorar e backup único. Migre para BullMQ quando houver evidência medida de que a fila prejudica o banco, ou quando o produto exigir agendamento que a biblioteca SQL não entrega. Se o Redis já existe e é bem operado, com persistência e memória dedicada à fila, BullMQ com outbox é uma escolha sólida. O que não vale é adotar Redis por reflexo e descobrir três meses depois que o pedido foi salvo e o e-mail nunca saiu.
Deixe um comentário