Padrão cron + banco de dados: trabalhos em segundo plano sem fila
Aprenda o padrão cron + banco de dados para executar trabalhos agendados em segundo plano com tentativas, bloqueio e idempotência — sem montar um sistema de filas completo.

O problema: trabalho agendado sem infraestrutura extra
A maioria dos apps precisa que algo aconteça mais tarde ou em um horário definido: enviar e-mails de acompanhamento, rodar uma checagem de cobrança noturna, limpar registros antigos, reconstruir um relatório ou atualizar um cache.
No começo, é tentador adicionar um sistema de filas completo porque parece a forma “certa” de fazer jobs em segundo plano. Mas filas adicionam peças móveis: mais um serviço para rodar, monitorar, deployar e depurar. Para uma equipe pequena (ou um fundador solo), esse peso extra pode te atrapalhar.
Então a pergunta real é: como executar trabalhos agendados de forma confiável sem montar mais infraestrutura?
Uma tentativa comum é simples: adicionar uma entrada no cron que chama um endpoint, e deixar esse endpoint fazer o trabalho. Funciona até não funcionar. Quando você tem mais de um servidor, um deploy no momento errado, ou um job que demora mais que o esperado, começam a aparecer falhas confusas.
Trabalhos agendados costumam quebrar de algumas formas previsíveis:
- Execuções duplicadas: dois servidores rodam a mesma tarefa, então notas fiscais são geradas duas vezes ou e-mails são enviados em duplicidade.
- Execuções perdidas: uma chamada do cron falha durante um deploy e ninguém percebe até os usuários reclamarem.
- Falhas silenciosas: o job dá erro uma vez e nunca mais roda porque não há plano de retry.
- Trabalho parcial: o job cai no meio do caminho e deixa os dados em estado inconsistente.
- Sem trilha de auditoria: você não consegue responder “quando isso rodou pela última vez?” ou “o que aconteceu ontem à noite?”.
O padrão cron + banco de dados é um caminho intermediário. Você continua usando cron para “acordar” no horário, mas armazena a intenção do job e o estado no banco de dados para que o sistema coordene, faça retries e registre o que aconteceu.
É uma boa opção quando você já tem um banco (frequentemente PostgreSQL), poucos tipos de jobs e quer comportamento previsível com mínimo trabalho de ops. Também é natural para apps construídos rapidamente em stacks modernos (por exemplo, React + Go + PostgreSQL).
Não é indicado quando você precisa de altíssimo throughput, jobs de longa execução que precisam transmitir progresso, ordenação estrita entre muitos tipos de jobs, ou fan-out pesado (milhares de subtarefas por minuto). Nesses casos, uma fila real e workers dedicados geralmente compensam.
A ideia central em linguagem simples
O padrão cron + banco de dados executa trabalho em segundo plano agendado sem rodar um sistema de filas completo. Você ainda usa cron (ou qualquer agendador), mas o cron não decide o que rodar. Ele apenas acorda um worker com frequência (uma vez por minuto é comum). O banco decide qual trabalho está devido e garante que apenas um worker pegue cada job.
Pense nisso como um checklist compartilhado em um quadro branco. O cron é a pessoa que entra na sala a cada minuto e pergunta: “Alguém precisa fazer algo agora?” O banco é o quadro branco que mostra o que está devido, o que já foi pego e o que foi feito.
Os componentes são diretos:
- Um gatilho do agendador roda com frequência.
- Uma tabela de jobs guarda o “o quê” e o “quando” (tempo devido), além do status e do contador de tentativas.
- Um ou mais workers consultam a tabela, reclamam um job e executam o trabalho.
- A requisição do job usa um bloqueio no banco para que dois workers não peguem a mesma linha.
- O banco permanece como fonte da verdade sobre o que rodou, o que falhou e o que deve retry.
Exemplo: você quer enviar lembretes de faturas todas as manhãs, atualizar um cache a cada 10 minutos e limpar sessões antigas à noite. Em vez de três comandos cron separados (cada um com seus modos de sobreposição e falha), você armazena entradas de job em um só lugar. O cron inicia o mesmo processo de worker. O worker pergunta ao Postgres “o que está devido agora?” e o Postgres responde permitindo que o worker reclame exatamente um job por vez.
Isso escala gradualmente. Você pode começar com um worker em um servidor. Mais tarde, rodar cinco workers em vários servidores. O contrato continua o mesmo: a tabela é o contrato.
A mudança de mentalidade é simples: o cron é só o despertador. O banco é o controlador de tráfego que decide o que pode rodar, registra o que aconteceu e te dá um histórico claro quando algo dá errado.
Projetando a tabela de jobs (um esquema prático)
Esse padrão funciona melhor quando seu banco vira a fonte da verdade sobre o que deve rodar, quando deve rodar e o que aconteceu da última vez. O esquema não é sofisticado, mas detalhes pequenos (campos de lock e índices certos) fazem grande diferença conforme a carga cresce.
Uma tabela ou duas?
Duas abordagens comuns:
- Uma tabela combinada quando você só se importa com o estado mais recente de cada job (simples, menos joins).
- Duas tabelas quando você quer separar “o que é esse job” e “cada vez que ele rodou” (melhor histórico, depuração mais fácil).
Se você espera depurar falhas com frequência, mantenha histórico. Se quer a configuração mínima possível, comece com uma tabela e adicione histórico depois.
Um esquema prático (versão com duas tabelas)
Aqui está um layout amigável ao PostgreSQL. Se você estiver construindo em Go com PostgreSQL, essas colunas mapeiam bem para structs.
-- What should exist (the definition)
create table job_definitions (
id bigserial primary key,
job_type text not null,
payload jsonb not null default '{}'::jsonb,
schedule text, -- optional: cron-like text if you store it
max_attempts int not null default 5,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- What should run (each run / attempt group)
create table job_runs (
id bigserial primary key,
definition_id bigint references job_definitions(id),
job_type text not null,
payload jsonb not null default '{}'::jsonb,
run_at timestamptz not null,
status text not null, -- queued | running | succeeded | failed | dead
attempts int not null default 0,
max_attempts int not null default 5,
locked_by text,
locked_until timestamptz,
last_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
Alguns detalhes que salvam dores depois:
- Mantenha job_type como uma string curta que você possa rotear (como
send_invoice_emails). - Armazene payload como
jsonbpara poder evoluí-lo sem migrations. - run_at é seu “próximo horário devido”. O cron (ou um script) define, os workers consomem.
- locked_by e locked_until permitem que workers reclamem jobs sem pisar uns nos outros.
- last_error deve ser curto e legível. Coloque stack traces em outro lugar se preciso.
Índices que você vai querer
Sem índices, os workers acabam escaneando demais. Comece com:
- Um índice para encontrar trabalho devido rapidamente:
(status, run_at) - Um índice para ajudar a detectar locks expirados:
(locked_until) - Opcional: um índice parcial para trabalho ativo somente (por exemplo, status em
queuedefailed)
Isso mantém a consulta “encontrar próximo job executável” rápida mesmo quando a tabela cresce.
Bloqueando e reclamando jobs com segurança
O objetivo é simples: muitos workers podem rodar, mas apenas um deve pegar um job específico. Se dois workers processarem a mesma linha, você terá e-mails duplicados, cobranças em duplicidade ou dados confusos.
Uma abordagem segura é tratar a reclamação do job como uma “licença” (lease). O worker marca o job como bloqueado por uma janela curta. Se o worker travar, a licença expira e outro worker pode pegá-lo. É isso que locked_until faz.
Use uma licença para que crashes não bloqueiem trabalho para sempre
Sem licença, um worker pode travar após bloquear um job e nunca desbloqueá-lo (processo morto, servidor reiniciado, deploy com problema). Com locked_until, o job fica disponível de novo quando o tempo passar.
Uma regra típica: um job pode ser reclamado quando locked_until é NULL ou locked_until <= now().
Reivindique jobs com um update atômico
O detalhe chave é reivindicar o job em uma única instrução (ou numa transação). Você quer que o banco seja o árbitro.
Aqui está um padrão comum do PostgreSQL: pegue um job devido, trave-o e retorne ao worker. (Esse exemplo usa uma única tabela jobs; a mesma ideia vale para job_runs.)
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'queued'
AND run_at <= now()
AND (locked_until IS NULL OR locked_until <= now())
ORDER BY run_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs j
SET status = 'running',
locked_until = now() + interval '2 minutes',
locked_by = $1,
attempts = attempts + 1,
updated_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;
Por que funciona:
FOR UPDATE SKIP LOCKEDpermite que múltiplos workers compitam sem travar uns aos outros.- A licença é definida no momento da reclamação, então outros workers a ignoram até expirar.
RETURNINGentrega a linha ao worker que venceu a disputa.
Quanto tempo deve durar a licença, e como renová-la?
Defina a licença maior que uma execução normal, mas curta o suficiente para que um crash recupere rápido. Se a maioria dos jobs termina em 10 segundos, uma licença de 2 minutos é suficiente.
Para tarefas longas, renove a licença enquanto você trabalha (heartbeat). Uma abordagem simples: a cada 30 segundos, estenda locked_until se você ainda for o dono do job.
- Comprimento da licença: 5x a 20x do tempo típico do job
- Intervalo de heartbeat: 1/4 a 1/2 da licença
- A atualização de renovação deve incluir
WHERE id = $job_id AND locked_by = $worker_id
Essa última condição importa. Ela impede que um worker estenda a licença de um job que não possui mais.
Retries e backoff que se comportam de forma previsível
Retry é onde esse padrão ou parece tranquilo ou vira uma bagunça barulhenta. O objetivo é simples: quando um job falha, tente de novo mais tarde de um modo que você consiga explicar, medir e parar.
Comece tornando o estado do job explícito e finito: queued, running, succeeded, failed, dead. Na prática, a maioria das equipes usa failed para “falhou mas vai tentar de novo” e dead para “falhou e desistimos”. Essa distinção evita loops infinitos.
Contar tentativas é a segunda proteção. Armazene attempts (quantas vezes tentou) e max_attempts (quantas vezes permite). Quando um worker captura um erro, ele deve:
- incrementar
attempts - definir o estado para
failedseattempts < max_attempts, caso contráriodead - calcular
run_atpara a próxima tentativa (apenas parafailed)
Backoff é só a regra que decide o próximo run_at. Escolha uma, documente-a e mantenha-a consistente:
- Atraso fixo: sempre esperar 1 minuto
- Exponencial: 1m, 2m, 4m, 8m
- Exponencial com teto: exponencial mas nunca mais que, digamos, 30m
- Adicione jitter: randomize um pouco para que jobs não tentem todos no mesmo segundo
Jitter importa quando uma dependência cai e volta. Sem ele, centenas de jobs podem tentar ao mesmo tempo e falhar de novo.
Armazene detalhe de erro suficiente para tornar falhas visíveis e depuráveis. Você não precisa de um sistema de logs completo, mas precisa do básico:
last_error(mensagem curta, segura para mostrar numa tela de admin)error_codeouerror_type(ajuda a agrupar)failed_atenext_run_at- opcional
last_stack(apenas se você controlar o tamanho)
Uma regra concreta que funciona bem: marque jobs como dead após 10 tentativas e use backoff exponencial com jitter. Isso mantém falhas transitórias sendo re-tentadas, mas evita que jobs quebrados consumam CPU para sempre.
Idempotência: evitar duplicatas mesmo se um job repetir
Idempotência significa que seu job pode rodar duas vezes e ainda produzir o mesmo resultado final. Nesse padrão, importa porque a mesma linha pode ser pega novamente após um crash, timeout ou retry. Se seu job é “enviar um e-mail de fatura”, executá-lo duas vezes não é inofensivo.
Uma forma prática de pensar: divida cada job em (1) fazer o trabalho e (2) aplicar um efeito. Você quer que o efeito aconteça uma vez, mesmo que o trabalho seja tentado várias vezes.
Use uma chave de idempotência ligada ao evento de negócio
Uma chave de idempotência deve vir do que o job representa, não da tentativa do worker. Boas chaves são estáveis e fáceis de explicar, como invoice_id, user_id + day ou report_name + report_date. Se duas tentativas do job se referem ao mesmo evento do mundo real, elas devem compartilhar a mesma chave.
Exemplo: “Gerar relatório diário de vendas para 2026-01-14” pode usar sales_report:2026-01-14. “Cobrar fatura 812” pode usar invoice_charge:812.
Faça "apenas uma vez" com restrições do banco
A proteção mais simples é deixar o PostgreSQL rejeitar duplicatas. Armazene a chave de idempotência em um campo indexável e adicione uma restrição única.
-- Example: ensure one logical job/effect per business key
ALTER TABLE jobs
ADD COLUMN idempotency_key text;
CREATE UNIQUE INDEX jobs_idempotency_key_uniq
ON jobs (idempotency_key)
WHERE idempotency_key IS NOT NULL;
Isso impede que duas linhas com a mesma chave existam ao mesmo tempo. Se seu design permite múltiplas linhas (para histórico), coloque a unicidade em uma tabela de “efeitos”, como sent_emails(idempotency_key) ou payments(idempotency_key).
Efeitos comuns para proteger:
- E-mails: crie uma linha
sent_emailscom chave única antes de enviar, ou registre o id da mensagem do provedor quando enviado. - Webhooks: armazene
delivered_webhooks(event_id)e pule se já existir. - Pagamentos: sempre use o recurso de idempotência do provedor de pagamentos além da sua chave única no banco.
- Escrita de arquivo: escreva em um nome temporário e depois renomeie, ou armazene um registro
file_generatedchaveado por(type, date).
Se você está em uma stack com Postgres (por exemplo, backend Go + PostgreSQL), essas checagens de unicidade são rápidas e fáceis de manter próximas aos dados. A ideia-chave é simples: retries são normais, duplicatas são opcionais.
Passo a passo: construa um worker e um agendador mínimos
Escolha um runtime simples e fique com ele. O ponto do padrão cron + banco é menos peças móveis, então um processo pequeno em Go, Node ou Python que fale com PostgreSQL costuma ser suficiente.
Construa em cinco passos pequenos
-
Crie as tabelas e índices. Adicione uma tabela
jobs(mais quaisquer tabelas de referência que quiser), indexerun_ate adicione um índice que ajude seu worker a encontrar jobs disponíveis rápido (por exemplo em(status, run_at)). -
Escreva uma função de enqueue mínima. Sua app deve inserir uma linha com
run_atdefinido como “agora” ou um horário futuro. Mantenha o payload pequeno e previsível (IDs e um job type, não blobs enormes).
INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
- Implemente o loop de claim. Rode em uma transação. Selecione alguns jobs devidos, trave-os para que outros workers os ignorem, e marque-os como
runningna mesma transação.
WITH picked AS (
SELECT id
FROM jobs
WHERE status = 'queued' AND run_at <= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id IN (SELECT id FROM picked)
RETURNING *;
-
Processe e finalize. Para cada job reclamado, faça o trabalho e depois atualize para
donecomfinished_at. Se falhar, registre uma mensagem de erro e mova de volta paraqueuedcom um novorun_at(backoff). Mantenha as atualizações de finalização pequenas e sempre as execute, mesmo se o processo estiver encerrando. -
Adicione regras de retry que você possa explicar. Use uma fórmula simples como
run_at = now() + (attempts^2) * interval '10 seconds', e pare apósmax_attemptsdefinindostatus = 'dead'.
Adicione visibilidade básica
Você não precisa de um dashboard completo no dia um, mas precisa do suficiente para perceber problemas.
- Logue uma linha por job: claimed, succeeded, failed, retried, dead.
- Crie uma consulta ou view simples para “dead jobs” e “old running jobs”.
- Alerta sobre contagens (por exemplo, mais que N dead jobs na última hora).
Se você já está numa stack Go + PostgreSQL, isso mapeia bem para um único binário de worker mais cron.
Um exemplo realista que você pode copiar
Imagine um SaaS pequeno com dois trabalhos agendados:
- Uma limpeza noturna que remove sessões expiradas e arquivos temporários antigos.
- Um e-mail semanal de “seu relatório de atividade” enviado a cada usuário toda segunda de manhã.
Mantenha simples: uma tabela PostgreSQL para conter jobs e um worker que roda a cada minuto (acionado por cron). O worker reclama jobs devidos, executa e registra sucesso ou falha.
O que é enfileirado e quando
Você pode enfileirar jobs de alguns lugares:
- Diário às 02:00: enfileirar um job
cleanup_nightlypara “hoje”. - No cadastro: enfileirar um job
send_weekly_reportpara a próxima segunda do usuário. - Após um evento (como “usuário clicou em Export”): enfileirar
send_weekly_reportque roda imediatamente para um intervalo de datas específico.
O payload é só o mínimo que o worker precisa. Mantenha pequeno para facilitar retry.
{
"type": "send_weekly_report",
"payload": {
"user_id": 12345,
"date_range": {
"from": "2026-01-01",
"to": "2026-01-07"
}
}
}
Como a idempotência evita envios duplicados
Um worker pode travar no pior momento: logo após enviar o e-mail, mas antes de marcar o job como “feito”. Ao reiniciar, ele pode pegar o mesmo job outra vez.
Para evitar envios duplicados, dê ao trabalho uma chave natural de deduplicação e armazene-a onde o banco possa aplicar uma restrição. Para relatórios semanais, uma boa chave é (user_id, week_start_date). Antes de enviar, o worker registra “vou enviar o relatório X”. Se esse registro já existir, pula o envio.
Isso pode ser tão simples quanto uma tabela sent_reports com uma constraint única em (user_id, week_start_date), ou uma idempotency_key única no job.
Como uma falha se parece (e como se recupera)
Suponha que o provedor de e-mail dê timeout. O job falha, então o worker:
- incrementa
attempts - salva a mensagem de erro para depuração
- agenda a próxima tentativa com backoff (por exemplo: +1 min, +5 min, +30 min, +2 horas)
Se continuar falhando além do limite (por exemplo, 10 tentativas), marque como “dead” e pare de tentar. O job ou tem sucesso uma vez, ou ele tenta de forma explícita e segura até o limite, e a idempotência torna o retry seguro.
Erros comuns e armadilhas
O padrão cron + banco é simples, mas pequenos erros podem gerar duplicatas, trabalho preso ou carga surpresa. A maioria dos problemas aparece depois do primeiro crash, deploy ou pico de tráfego.
Erros que causam duplicatas ou jobs presos
A maioria dos incidentes reais vem de algumas armadilhas:
- Rodar o mesmo job de múltiplas entradas do cron sem uma licença. Se dois servidores dispararem no mesmo minuto, ambos podem tentar pegar o mesmo trabalho, a menos que o passo de claim seja atômico e defina um lock (ou lease) na mesma transação do banco.
- Ignorar
locked_until. Se um worker travar depois de reclamar um job, aquela linha pode ficar “em progresso” para sempre. Um timestamp de lease permite que outro worker a pegue depois. - Retentar instantaneamente após falha. Quando uma API cai, retries instantâneos criam picos, consomem rate limits e continuam falhando em loop. Sempre agende a próxima tentativa para algum tempo no futuro.
- Tratar “pelo menos uma vez” como “exatamente uma vez”. Um job pode rodar duas vezes (timeouts, reinícios de worker, problemas de rede). Se rodar duas vezes for prejudicial, torne os efeitos seguros para repetição.
- Armazenar payloads enormes na linha do job. Blobs JSON grandes incham a tabela, tornam índices lentos e tornam o lock mais pesado. Armazene uma referência (como
user_id,invoice_idou uma chave de arquivo) e busque o resto na execução.
Exemplo: você envia um e-mail semanal de fatura. Se o worker der timeout após enviar mas antes de marcar o job como feito, o mesmo job pode ser re-tentado e enviar um e-mail duplicado. Isso é normal nesse padrão, a menos que você adicione uma proteção (por exemplo, registrar um evento único “e-mail enviado” chaveado pelo id da fatura).
Armadilhas menos óbvias
Evite misturar agendamento e execução numa mesma transação longa. Se você segurar uma transação aberta enquanto faz chamadas de rede, mantém locks por mais tempo que o necessário e bloqueia outros workers.
Fique atento a diferenças de relógio entre máquinas. Use o tempo do banco (NOW() no PostgreSQL) como fonte de verdade para run_at e locked_until, não o relógio do app server.
Defina um tempo máximo de execução claro. Se um job pode levar 30 minutos, faça a licença maior que isso e renove se necessário. Caso contrário outro worker pode pegá-lo no meio da execução.
Mantenha a tabela de jobs saudável. Se jobs completados se acumularem para sempre, consultas ficam lentas e a contenção aumenta. Escolha uma regra simples de retenção (arquivar ou deletar linhas antigas) antes que a tabela vire um problema.
Checklist rápido e próximos passos
Checklist rápido
Antes de colocar esse padrão em produção, verifique o básico. Uma pequena omissão aqui costuma virar jobs presos, duplicatas surpresas ou workers dando hammer no banco.
- Sua tabela de jobs tem o essencial:
run_at,status,attempts,locked_untilemax_attempts(além delast_errorou similar para ver o que aconteceu). - Cada job pode rodar duas vezes sem dano. Se não tiver certeza, adicione uma chave de idempotência ou uma regra de unicidade em torno do efeito (por exemplo, uma fatura por
invoice_id). - Há um lugar claro para observar falhas e decidir o que fazer: visualizar jobs falhos, reexecutar um job ou marcá-lo como dead quando deve parar de tentar.
- Seu timeout de lease é sensato. Deve ser longo o suficiente para execuções normais, mas curto o bastante para que workers travados não bloqueiem progresso por horas.
- O backoff de retry é previsível. Deve desacelerar falhas repetidas e parar após
max_attempts.
Se isso estiver certo, o padrão cron + banco de dados costuma ser estável para cargas reais.
Próximos passos
Depois que o checklist estiver ok, foque na operação do dia a dia.
- Adicione duas ações administrativas pequenas: “retry now” (define
run_at = now()e limpa o lock) e “cancel” (move para um status terminal). Isso economiza tempo em incidentes. - Faça o worker logar uma linha por job: tipo, id do job, número da tentativa e resultado. Adicione alerta em contagens crescentes de falhas.
- Teste carga com um pico realista: muitos jobs agendados para o mesmo minuto. Se o claim estiver lento, adicione o índice certo (frequentemente
status, run_at).
Se quiser montar isso rapidamente, Koder.ai (koder.ai) pode ajudar você a ir do esquema a um app Go + PostgreSQL implantado com menos trabalho manual, enquanto você foca em regras de lock, retries e idempotência.
Se depois você ultrapassar essa configuração, você terá aprendido o ciclo de vida do job claramente, e essas mesmas ideias se mapeiam bem para um sistema de filas completo.
Perguntas frequentes
Quando devo usar cron com um banco de dados para tarefas em segundo plano?
Use esse padrão quando você já tem um banco de dados, poucos tipos de tarefas e quer executar trabalhos agendados sem operar uma fila separada. Ele é adequado para tarefas como lembretes por e-mail, limpeza, relatórios e atualização de cache.
O que o cron faz nessa configuração?
O cron deve apenas iniciar o worker em um intervalo regular, geralmente uma vez por minuto. O banco de dados decide quais tarefas estão pendentes, registra o estado delas e permite que um worker assuma cada tarefa.
Quais campos uma tabela de tarefas deve incluir?
Armazene o tipo da tarefa, uma pequena carga JSON, o horário previsto, o status, a contagem de tentativas, o limite de novas tentativas, o proprietário do bloqueio, a expiração do bloqueio e uma mensagem de erro curta. Esses campos dão aos workers informações suficientes para executar e recuperar tarefas com segurança.
Como vários workers evitam executar a mesma tarefa?
Assuma tarefas em uma única transação de banco de dados com bloqueio de linhas, como FOR UPDATE SKIP LOCKED do PostgreSQL. Atualize a linha para em execução e defina sua concessão antes de o worker iniciar a tarefa de fato.
Por que tarefas em segundo plano precisam de um tempo limite de concessão?
Uma concessão é um bloqueio temporário registrado com locked_until. Se um worker falhar, a concessão expira e outro worker pode assumir a tarefa, em vez de ela ficar travada para sempre.
Como as tarefas com falha devem tentar novamente?
Tente novamente mais tarde seguindo uma regra clara de espera progressiva, como 1 minuto, 2 minutos, 4 minutos e depois um atraso limitado com um pouco de aleatoriedade. Após o número permitido de tentativas, marque a tarefa como encerrada para que ela deixe de consumir o tempo dos workers.
O que é idempotência e por que ela importa?
Torne o efeito colateral seguro para repetir. Por exemplo, use uma chave de idempotência estável, como um ID de fatura ou um ID de usuário mais a data do relatório, e então imponha a unicidade no PostgreSQL ou no provedor externo.
Esse padrão garante processamento exatamente uma vez?
Não. Um worker pode falhar depois de enviar um e-mail ou chamar um provedor de pagamentos, mas antes de registrar o sucesso. Projete para processamento pelo menos uma vez e use idempotência para evitar efeitos duplicados.
Um worker deve manter uma transação de banco de dados aberta enquanto executa a tarefa?
Mantenha as transações de banco de dados curtas. Assuma a tarefa, confirme a transação, faça chamadas de rede fora dela e então registre o sucesso ou a falha em uma atualização separada. Transações longas mantêm bloqueios e atrasam outros workers.
Como monitoro tarefas do cron e do banco de dados?
Acompanhe tarefas encerradas e tarefas antigas ainda em execução, registre cada atribuição e resultado e configure alertas quando as falhas aumentarem. Adicione ações administrativas simples para tentar uma tarefa novamente agora, cancelá-la ou inspecionar seu erro mais recente.