A fila de jobs entra no projeto como detalhe e vira gargalo seis meses depois. A pergunta real não é “qual é mais rápido”, e sim: o job precisa nascer na mesma transação que o dado de negócio? Se a resposta é sim, o Postgres já resolveu metade do problema. Se é não, Redis (ou um broker dedicado) tende a custar menos operação.
O que muda quando o job vive no mesmo banco
Com a fila numa tabela, o INSERT do pedido e o INSERT do job de e-mail de confirmação acontecem no mesmo COMMIT. Não existe janela em que o pedido foi gravado e o job se perdeu, nem o inverso: job enfileirado para um pedido que sofreu rollback. Com Redis, essa garantia vira responsabilidade da aplicação. O padrão usual é o outbox: gravar o evento numa tabela e ter um processo que o empurra para o Redis. Ou seja, você acaba mantendo a tabela de qualquer forma.
O consumo concorrente virou trivial desde que FOR UPDATE SKIP LOCKED existe. Cada worker pega um lote de linhas sem bloquear os demais, e uma linha travada por um worker que morreu volta sozinha quando a conexão cai.
SELECT id, payload FROM jobs WHERE status = 'pendente' AND executar_em <= now() ORDER BY id LIMIT 20 FOR UPDATE SKIP LOCKED; Marque como em_execucao na mesma transação e faça o commit antes de processar, guardando o horário para o reaproveitamento de jobs órfãos.Onde o Postgres começa a pesar
O custo aparece em três pontos. Primeiro, o churn: uma tabela em que toda linha é inserida, atualizada duas vezes e apagada gera muitas tuplas mortas, e o autovacuum precisa acompanhar. Particionar por dia e descartar partições antigas em vez de DELETE resolve na maioria dos casos. Segundo, o polling: se os workers consultam a cada segundo, você paga latência de até um segundo e carga constante. LISTEN/NOTIFY reduz a latência, mas não é durável, então continue com o polling como rede de segurança. Terceiro, a disputa com o tráfego transacional: job pesado e consulta de tela compartilham conexões e I/O.
Uma ordem de grandeza útil: até algumas centenas de jobs por segundo, uma tabela bem indexada em instância dimensionada normalmente aguenta sem drama. Passando de milhares por segundo sustentados, ou com jobs de segundos de duração segurando conexões, o banco deixa de ser o lugar certo. Esse limite varia com hardware e tamanho do payload, então meça com o seu volume antes de decidir.
O que o Redis entrega e o que ele cobra
Redis dá latência de milissegundos, estruturas prontas para delay e prioridade, e tira a carga de fila do banco principal. Bibliotecas maduras (BullMQ no Node, Sidekiq no Ruby, Celery com Redis no Python) trazem retry, backoff, agendamento e painel prontos. O que cobra: mais uma peça para monitorar, persistência que precisa ser configurada com consciência (AOF, e a diferença entre perder um segundo de jobs e não perder nenhum) e a consistência com o banco que você mesmo precisa construir.
Um ponto que costuma ser subestimado: reprocessamento e auditoria. Numa tabela, “quais jobs falharam ontem e por quê” é um SELECT. No Redis, é uma questão de quanto histórico a biblioteca guarda e por quanto tempo.
Comparativo direto
| Critério | Fila no Postgres | Fila no Redis |
|---|---|---|
| Atomicidade com o dado de negócio | Nativa, mesmo commit | Exige outbox ou aceitar perda rara |
| Latência de despacho | Polling; de dezenas de ms a ~1 s | Milissegundos |
| Volume sustentado | Centenas/s com folga; milhares pedem partição e cuidado | Dezenas de milhares/s |
| Operação | Nenhuma peça nova | Mais um serviço, backup e monitoramento |
| Auditoria e reprocesso | SQL puro | Depende da biblioteca |
Falhas, retry e o que acontece às três da manhã
Fila boa se define pelo comportamento quando algo dá errado. Três casos merecem desenho explícito. Job que falha por erro transitório (timeout, 503 de terceiro) precisa de retry com backoff exponencial e jitter, senão uma indisponibilidade de dez minutos gera uma tempestade de reenvios no minuto em que o serviço volta. Job que falha por erro permanente (payload inválido) precisa ir para uma dead letter após N tentativas, com o erro guardado, e não voltar ao ciclo para sempre. E job que fica preso porque o worker morreu no meio precisa de timeout de visibilidade: se não houve heartbeat em X segundos, ele volta para pendente.
No Postgres, tudo isso cabe em três colunas (tentativas, proxima_tentativa, erro) e uma consulta de recuperação. No Redis, as bibliotecas trazem esse comportamento pronto, mas os parâmetros padrão raramente combinam com o seu caso. Leia o default de retry antes de subir para produção, porque o número de tentativas padrão de várias delas é maior do que a maioria dos provedores externos tolera sem bloquear você.
Idempotência: a exigência que os dois cobram
Nenhuma das duas opções entrega “exatamente uma vez” de verdade. O que existe é pelo menos uma vez, e o consumidor precisa tolerar repetição. Na prática, isso significa uma chave de idempotência por job (ID do pedido mais o tipo da ação) gravada na mesma transação do efeito colateral. Cobrança sem chave de idempotência é o incidente esperando sexta-feira à tarde. Se o job chama uma API externa que aceita Idempotency-Key, passe-a. Se não aceita, registre a resposta antes de considerar o job concluído e consulte esse registro no retry.
O que diferencia as duas opções aqui é onde a chave mora. Com a fila no Postgres, a checagem de duplicata e a mudança de estado do job podem acontecer no mesmo commit. Com Redis, você precisa de um segundo armazenamento para a checagem, ou de uma restrição única no banco de negócio, o que significa acoplar o consumidor ao banco de qualquer forma.
Observabilidade que precisa existir desde o primeiro deploy
Quatro métricas bastam para começar: profundidade da fila (jobs pendentes), idade do job mais antigo pendente, taxa de falha por tipo de job e duração por tipo. A idade do mais antigo pega problemas que a profundidade esconde, como um único tipo de job travando um worker. Alerte por idade, não por tamanho. No Postgres, essas métricas saem de uma consulta agregada, e um painel simples resolve. No Redis, elas vêm da biblioteca ou do exporter, e cada uma tem sua peculiaridade.
Um teste barato antes de decidir: escreva o job mais pesado do sistema, dispare mil deles e observe o banco principal por dez minutos. Se a latência das consultas de tela mudou de forma perceptível, você já sabe que a fila não pode morar ali.
Custo de migrar depois
Se a interface é enfileirar(tipo, payload, opcoes) e nenhum código de negócio conhece o driver, a troca de backend é uma tarefa de dias. Se o código chama a biblioteca de fila em cinquenta lugares, é um projeto. Defina a interface desde o começo, mesmo que só haja uma implementação.
Onde cravar cada um
Use Postgres quando o time é pequeno, o volume é moderado e a perda de um job significa dinheiro ou dado inconsistente: cobrança, emissão de nota, e-mail transacional, sincronização com ERP. A ausência de uma peça nova pesa mais que a latência extra.
Use Redis (ou RabbitMQ, se roteamento e ack entre serviços forem o centro) quando os jobs são muitos, curtos e toleram repetição: geração de thumbnail, webhook de saída com retry, invalidação de cache, envio de notificação em massa. Aí a idempotência do consumidor importa mais que a atomicidade da criação.
O erro comum é começar com Redis por hábito e, dois meses depois, escrever a tabela de outbox porque um pedido ficou sem e-mail. Começar em Postgres com a interface da fila isolada atrás de uma função enfileirar() custa uma tarde para migrar depois, se o volume exigir. O caminho inverso, tirar Redis de um sistema que assumiu garantias que ele não dava, custa bem mais.
Deixe um comentário