Imagine a seguinte situação comum no dia a dia da engenharia de software: você precisa de um cron job que, a cada 5 minutos, verifica no banco de dados se existem registros que atendem uma condição e, se existirem, executa algumas ações neles.
À primeira vista, a solução parece trivial: basta agendar uma função que executa um SELECT * WHERE status = 'PENDING', itera sobre os resultados e dispara o processamento. No entanto, quando esse cenário vai para produção — com múltiplas instâncias da aplicação, volume crescente de dados e integrações instáveis —, a abordagem ingênua rapidamente resulta em race conditions, estouro de memória, travamento do banco e duplicação de efeitos colaterais.
A escolha da abordagem ideal depende diretamente da origem da condição e da volatilidade do volume de dados. A seguir, analisamos os riscos do modelo tradicional e como implementar soluções de alta resiliência.
5 min
intervalo típico de varredura periódica
100%
de risco de race condition sem lock distribuído
0 ms
overhead no banco com modelo Event-Driven
Resumo em 30 segundos
- O risco da abordagem ingênua: Executar consultas abertas periodicamente sem controle de concorrência provoca sobreposição de rotinas, consumo excessivo de memória e processamento duplicado.
- Abordagem A (Polling Robusto): Indicada para condições puramente temporais. Exige Lock Distribuído para a tarefa, processamento em lotes com
FOR UPDATE SKIP LOCKEDe transição rigorosa de estados. - Abordagem B (Event-Driven / Delayed Messaging): A alternativa superior para reações a ações do usuário. Substitui a varredura no banco por filas de mensagens diferidas (Delayed Queues) e o padrão Transactional Outbox.
- Princípio da Idempotência: A ação executada no worker deve ser capaz de lidar com re-tentativas sem gerar efeitos colaterais duplicados (ex: cobranças duplas ou e-mails repetidos).
01 · Contexto
O que costuma dar errado na abordagem ingênua?
No início de um projeto, buscar todos os registros elegíveis a cada 5 minutos funciona perfeitamente. No entanto, conforme o sistema escala, quatro gargalos graves passam a afetar a infraestrutura:
Falha 1
Sobreposição (Overlapping)
Se o lote de registros crescer e o processamento demorar mais do que 5 minutos, o próximo ciclo do cron iniciará antes do término do anterior, criando um efeito de bola de neve que consome os recursos da aplicação.
Falha 2
Múltiplas Instâncias
Se a aplicação rodar em um cluster com 5 pods/containers (Kubernetes), todas as 5 instâncias dispararão o cron no mesmo instante, selecionando os mesmos registros e disparando ações duplicadas.
Falha 3
Gargalo de Memória e Banco
Consultas do tipo SELECT * sem paginação em tabelas com centenas de milhares de linhas geram picos abruptos de I/O de disco, locking de tabela e picos de memória (OOM) no worker.
Falha 4
Acoplamento Temporal
Registros que entram na condição precisam esperar até 5 minutos para serem processados, criando um atraso desnecessário se a regra de negócio exigir resposta rápida.
02 · Polling Robusto
Abordagem A: Polling Robusto em Banco de Dados
Se a condição for puramente baseada em tempo (ex: "cancelar pedidos pendentes há mais de 24 horas") e o cron job for a única opção viável, a consulta deve ser estruturada para garantir concorrência segura e alto desempenho.
1. Lock Distribuído
Para impedir que múltiplas instâncias executem o agendador simultaneamente, deve-se implementar uma trava centralizada via chave temporária. Ferramentas como Redis (Redlock), ShedLock ou PostgreSQL Advisory Locks garantem que apenas um nó execute a tarefa naquele intervalo de 5 minutos.
2. Batching com FOR UPDATE SKIP LOCKED
Em vez de ler a tabela inteira, o worker processa dados em pequenos lotes (ex: 100 em 100 registros), bloqueando individualmente as linhas que está operando sem travar as consultas paralelas do restante do sistema:
O modificador SKIP LOCKED instrui o banco de dados a ignorar automaticamente os registros que já estão sendo processados por outras threads ou instâncias secundárias. Isso permite escalar horizontalmente o número de workers sem gerar conflito ou sobreposição de dados.
03 · Event-Driven
Abordagem B: Arquitetura Orientada a Eventos e Mensageria
Na maioria dos sistemas modernos, o polling a cada 5 minutos pode — e deve — ser substituído por um modelo Event-Driven. Se a condição no banco de dados muda em resposta a uma ação (ex: finalização de cadastro, alteração de status ou emissão de nota), a aplicação não deve consultar o banco continuamente para descobrir a mudança.
Padrão 1
Transactional Outbox Pattern
No momento em que o registro principal é gravado, insere-se um evento em uma tabela de outbox dentro da mesma transação. Um relay leve publica essa mensagem em um broker (RabbitMQ, SQS ou Kafka), eliminando locks no banco principal.
Padrão 2
Delayed Delivery / Message Queues
Se a regra exige aguardar exatamente 5 minutos após um evento para executar uma ação, o evento é publicado diretamente em uma fila de mensagens diferidas (Delay Queue do AWS SQS ou plugin de atraso do RabbitMQ).
04 · Comparativo
Qual abordagem escolher? Matriz de Decisão
A tabela a seguir resume quando utilizar cada estratégia com base nos requisitos arquiteturais da sua aplicação:
05 · Idempotência
A regra de ouro: Idempotência e Tratamento de Erros
Independentemente da abordagem escolhida (Polling ou Filas), o worker que executa a ação deve ser estritamente idempotente. Falhas de rede, timeouts de APIs externas e reinicializações de contêineres podem fazer com que a mesma tarefa seja entregue duas vezes.
- Chave de Idempotência: Utilize uma chave única (ex:
uuid_tarefa + tentativa) para evitar disparos repetidos de e-mails ou cobranças em gateways. - Circuit Breaker & Retry Limit: Se uma integração externa falhar durante a execução da ação, defina um limite claro de retentativas com exponential backoff.
- Dead Letter Queue (DLQ) / Status FAILED: Registros que falharem consecutivamente devem ser marcados como
FAILEDou enviados para uma DLQ, permitindo auditoria sem travar os próximos ciclos do cron.
06 · FAQ
Perguntas Frequentes sobre Cron Jobs e Banco de Dados
O comando FOR UPDATE SKIP LOCKED funciona em qual banco de dados?
O recurso é suportado nativamente pelos principais bancos relacionais modernos, como PostgreSQL (a partir da versão 9.5), MySQL (a partir da versão 8.0), Oracle Database e SQL Server (via READPAST).
Por que não usar apenas um flag Booleano como processado = true?
Atualizar uma flag booleana após o processamento abre margem para race conditions entre a leitura do registro e o momento da escrita. Se a ação demorar alguns segundos, outro worker lerá o registro com processado = false e executará a ação novamente. A transição deve ocorrer no nível de linha no momento da seleção.
Como garantir que apenas uma instância do Kubernetes rode o cron?
A solução recomendada é utilizar locks distribuídos (como Redis ou ShedLock). Alternativamente, em arquiteturas Kubernetes, pode-se separar a rotina agendada em um recurso do tipo CronJob isolado, desacoplado dos Pods da API principal.
FATTO Consultoria e Sistemas
© 2026 FATTO Consultoria e Sistemas. Todos os direitos reservados.
