06 de set. de 2025·7 min de leitura

Cron jobs serverless: evitar sobreposições e detectar falhas silenciosas

Torne cron jobs serverless confiáveis: escolha um agendador, bloqueie execuções concorrentes com locks e adicione um heartbeat de último run com alertas.

Cron jobs serverless: evitar sobreposições e detectar falhas silenciosas

O problema: sobreposições e falhas silenciosas

A maioria das tarefas cron serverless falha de duas formas previsíveis: elas rodam duas vezes ao mesmo tempo, ou param de rodar e ninguém percebe.

Uma sobreposição acontece quando a próxima execução agendada começa antes da anterior terminar. Em sistemas reais isso vira faturas duplicadas, e-mails repetidos, pagamentos em dobro ou importações que gravam as mesmas linhas duas vezes. Mesmo se seu código for em grande parte idempotente, sobreposições ainda atrapalham: gastam limites de taxa, cobram cartões em duplicidade e mantêm locks por mais tempo do que o esperado.

Falhas silenciosas são piores porque parecem que nada aconteceu. Um job pode parar porque um deploy removeu a agenda, uma mudança de permissão bloqueou o banco, um segredo expirou, quotas foram atingidas ou uma atualização da plataforma desativou um gatilho. Logs antigos podem continuar lá, então tudo parece bem até um cliente reportar dados faltantes.

"Funcionou uma vez" não é um plano de confiabilidade. Um job que rodou ontem não prova que vai rodar amanhã, especialmente quando config, permissões, segredos e runtimes mudam sem tocar no código.

O que você quer é simples e mensurável:

  • Sem execuções concorrentes (uma execução fica com o trabalho, as outras recuam)
  • Detecção rápida quando as execuções param (um sinal claro de "última execução", mais um alerta quando ficar obsoleto)

Trate o agendamento como infraestrutura de produção e você para de correr atrás de bugs intermitentes mais tarde.

Escolha um agendador que caiba na tarefa

Nem todos os agendadores se comportam igual, e isso importa quando pessoas dependem dos seus jobs.

Agendadores baseados em eventos disparam um gatilho em (mais ou menos) um horário específico e passam o trabalho para uma função ou endpoint. São simples e baratos, mas a entrega costuma ser "best effort" a não ser que você adicione retries, dead-letter e monitoramento.

Agendadores baseados em filas enfileiram uma mensagem "execute este job". Essa etapa extra é útil porque filas geralmente dão mais controle sobre retries, backpressure e visibilidade. Se seu job é pesado, lento ou com picos, uma fila tende a tornar falhas mais visíveis e fáceis de recuperar.

Opções comuns incluem AWS EventBridge Scheduler (ou CloudWatch schedules), GCP Cloud Scheduler (frequentemente junto com Pub/Sub ou Cloud Tasks), Azure Functions Timer Trigger (ou Logic Apps), e agendadores de CI como GitHub Actions para tarefas leves de manutenção.

Uma forma prática de escolher é responder algumas perguntas:

  • Com que frequência roda e o minuto exato importa?
  • Quanto tempo uma execução pode levar no pico (segundos vs horas)?
  • O que deve acontecer em caso de falha: retry, alerta ou ambos?
  • Que permissões precisa (banco, segredos, APIs de terceiros)?
  • Precisa recuperar execuções perdidas?

Se você precisa de garantias fortes, evite setups "dispare e esqueça" sem retries, sem dead-letter e sem alerta quando nada roda. É assim que jobs param silenciosamente por dias.

Defina seu modelo de execução antes de codar

Muitos problemas de confiabilidade começam antes do agendador. Começam com uma definição vaga do que um "run" significa.

Decida o que conta como uma execução e escreva isso. É "tudo desde a última execução", "todos os registros de ontem" ou "um batch id criado às 02:00"? Essa escolha afeta como você faz locks, retries e recuperação.

Torne "rodar duas vezes" seguro quando possível

Mesmo com bons locks, assuma que uma execução pode acontecer duas vezes por retries, timeouts ou replays manuais. Mire em trabalho idempotente: a mesma entrada deve levar ao mesmo resultado final.

Um padrão simples é armazenar uma chave de execução (como 2026-01-20) e registrar quais itens foram processados sob essa chave. Se a mesma chave rodar de novo, você pula itens já completos em vez de repetir efeitos colaterais.

Separe trigger do worker

Trate o gatilho do agendamento como um iniciador leve e coloque o trabalho real em uma função worker. O trigger deveria só calcular a chave de execução, tentar reclamar a execução e passar adiante.

Isso mantém a lógica de negócio separada das proteções de confiabilidade e facilita trocar de agendador depois.

Antes de codar, defina os resultados:

  • Sucesso: que dados estão definitivamente corretos e onde estão registrados?
  • Falha: o que deve ser rollbackado e o que pode ser refeito?
  • Sucesso parcial: o que é seguro manter e como retomar?
  • Timeout: que estado pode ficar para trás?

Planeje sua guarda de concorrência (estratégia de locking)

Se você roda cron jobs serverless, assuma que dois dias ruins vão acontecer: um job demora e a próxima agenda dispara mesmo assim, ou uma função re-tenta após timeout e você recebe duas cópias. Uma guarda de concorrência é a pequena peça que torna esses dias entediantes.

Comece escolhendo onde o lock fica. Escolha algo que seu job possa ler/escrever rápido, com comportamento forte de "apenas um vence".

Escolha sua loja de lock

Escolhas comuns:

  • Uma única linha de banco (ótimo se você já usa Postgres/MySQL e pode fazer um update atômico)
  • Redis (rápido e conveniente para locks curtos, mas garanta alta disponibilidade)
  • Lease em armazenamento de objetos (um blob/arquivo criado com "se não existe"; simples, mas pode ser mais lento)

Em seguida, decida o que a chave de lock representa. Uma chave prática muitas vezes inclui o nome do job e a janela agendada, como billing-sync:2026-01-20T02:00Z. Isso bloqueia duplicatas para o mesmo slot sem impedir a execução do próximo dia.

Sempre defina um TTL (expiração). TTL protege quando uma execução trava no meio ou a plataforma mata o processo. Defina um pouco maior que seu pior caso de runtime, não a média.

Finalmente, decida o que acontece em conflito:

  • Pular (seguro para trabalho idempotente, mas você pode perder uma execução)
  • Reagendar (melhor cobertura, mas pode gerar picos de tráfego)
  • Falhar alto (melhor para jobs críticos onde perder uma execução é pior que ruído)

Passo a passo: evitar execuções concorrentes com um lock

Sobreposições acontecem quando seu agendador dispara duas vezes, ou uma execução demora mais que o esperado. Em serverless, o conserto mais simples é um lock compartilhado armazenado fora da função (uma linha de DB, chave Redis ou KV gerenciado). Uma execução ganha o lock; as outras saem.

1) Use um fluxo de lock claro

Mantenha o fluxo previsível:

  • Adquirir lock (criação atômica ou update condicional)
  • Se o lock estiver tomado, sair rápido
  • Rodar o job
  • Liberar o lock, mas apenas se você ainda for o dono

2) Adicione um token de dono e sempre libere

Um token de dono evita que a Execução B libere o lock da Execução A. Sempre libere em um bloco finally para que erros não deixem um lock permanente.

import crypto from "crypto";

export async function handler() {
  const lockKey = "nightly-report";
  const owner = crypto.randomUUID();
  const ttlSeconds = 15 * 60; // lock safety window

  const acquired = await acquireLock({ lockKey, owner, ttlSeconds });
  if (!acquired) return { status: "skipped", reason: "lock_taken" };

  try {
    await doWork();
    return { status: "ok" };
  } finally {
    await releaseLock({ lockKey, owner }); // only release if owner matches
  }
}

Um bom acquireLock é atômico e define uma expiração (TTL) para que uma execução morta não bloqueie para sempre.

3) Teste com sobreposição forçada

Dispare duas execuções ao mesmo tempo (invoque manualmente duas vezes ou reduza o agendamento temporariamente). Uma deve rodar; a outra deve logar "skipped: lock_taken". Se ambas rodarem, sua escrita do lock não é verdadeiramente atômica ou falta a checagem do owner.

Passo a passo: adicionar um cheque de heartbeat de último run

Corrigir auth e segredos
Corrigimos drift de permissões, autenticação quebrada e segredos expirados que quebram workers agendados.

Um heartbeat é um registro pequeno de "eu rodei" que seu job grava sempre que termina (sucesso ou falha). Ele transforma falhas silenciosas em alertas, o que importa em serverless onde não há um processo sempre ligado para notar uma parada.

1) Escolha onde armazenar o "último run"

Escolha um lugar fácil de escrever, rápido de ler e improvável de cair junto com seu agendador:

  • Uma tabela de banco
  • Uma entrada em key-value (simples "job_name -> last_run")
  • Um sistema de métricas / série temporal

2) Grave os campos certos

Não armazene só um timestamp. Armazene o suficiente para debugar sem vasculhar logs primeiro.

job_name, run_id, started_at, finished_at, status, duration_ms, error_snippet

Uma regra prática: escreva uma vez no início (status=running) e atualize no fim (status=success ou failed). Isso também permite detectar "preso em running".

3) Defina um limiar e regras de alerta

Configure o limiar de "heartbeat ausente" para cerca de 2x o seu intervalo esperado. Se um job roda a cada 15 minutos, alerte se não houver heartbeat de sucesso em 30 minutos.

Separe tipos de alerta:

  • Heartbeat ausente: sem sucesso dentro do limiar (provavelmente não está rodando)
  • Falhas repetidas: últimas N execuções falharam (roda, mas o trabalho está quebrado)

Exemplo: uma sincronização noturna de faturamento deve rodar às 2:00 AM e terminar em 5 minutos. Alerte se não houver sucesso às 2:15 AM. Use outro alerta se rodou mas falhou três noites seguidas.

Logs e alertas que são realmente úteis

Quando cron jobs serverless se comportam mal, o primeiro problema geralmente não é o agendador. É que ninguém consegue responder rapidamente três perguntas: ele iniciou, terminou e por que pulou?

Dê a cada execução um run id consistente (por exemplo: timestamp + sufixo aleatório curto). Logue no início e inclua esse id em cada linha de log para seguir uma execução ponta a ponta.

Também logue quando uma execução não acontece de propósito. Uma execução pulada não é igual a uma falha, mas ainda é um sinal importante. Se o job não rodou porque não conseguiu o lock, diga isso claramente e inclua a chave do lock (e o owner se houver).

Mantenha logs consistentes:

  • Start: run id, nome do job, horário do trigger, versão/commit, inputs importantes
  • Skip: run id, nome do job, razão do skip (conflito de lock, scheduler desabilitado, feature flag off)
  • Finish: run id, status (ok/failed), duração, contagens (itens processados, erros)
  • Failure: run id, tipo de erro, contexto seguro e o que já foi feito

Alertas devem observar padrões, não só erros pontuais. Um pico de duração pode indicar uma API upstream lenta. Muitos skips podem indicar um lock preso. Ausência total de runs geralmente aponta para drift de permissões do scheduler ou um deploy que removeu o gatilho.

Faça todo alerta acionável. Inclua o último horário de sucesso, o próximo horário esperado e a primeira coisa a checar (registro de lock, status do scheduler, deploys recentes).

Retries, timeouts e recuperar com segurança

Entregue runs noturnos confiáveis
Faça sincronias de faturamento, exports e imports rodarem uma vez só com um registro confiável de último sucesso.

Retries ajudam, mas também geram sobreposições. Muitos agendadores retryam automaticamente se sua função retorna erro ou estoura tempo. Sem um lock distribuído (ou se você liberar cedo demais), um retry pode iniciar enquanto a execução original ainda está trabalhando.

Timeouts pioram isso. Em serverless, a plataforma pode parar seu código no meio quando você atinge o limite. Pode não haver chance de limpar, e você pode não saber quais passos terminaram. Se o agendador re-tentar, você pode enviar e-mails duplicados, cobrar em dobro ou gravar duplicados.

Uma abordagem mais segura é tornar cada execução retomável e idempotente. Pense em checkpoints, não numa única função "faz tudo". Por exemplo, um job noturno de faturamento pode armazenar um marcador de progresso como "processado até invoice_id 18420" e continuar dali.

Guardrails que impedem que retries e catch-ups causem danos:

  • Segure o lock durante toda a execução. Liberar só quando realmente terminar.
  • Registre um run_id e marcadores de progresso para que um retry possa continuar em vez de reiniciar.
  • Divida o trabalho em pequenos lotes com checagem por item "já processado".
  • Adicione um modo de backfill controlado que processa janelas perdidas uma a uma.

Backfills importam porque agendas falham. Se a execução de ontem falhou, a de hoje não deve automaticamente processar dois dias se seu sistema não aguentar. Uma regra simples: "procure a janela mais antiga faltante e pare".

Erros comuns e correções fáceis

A maioria das falhas em produção vem de algumas escolhas que parecem ok num protótipo.

  • TTL menor que o job. Sua promessa de "uma execução" quebra quando uma execução fica lenta. Correção: defina TTL para o pior caso de runtime mais uma margem e renove-o enquanto o job estiver vivo.
  • TTL muito longo. Um crash pode bloquear a agenda por horas. Correção: mantenha TTL razoável, libere em finally e use token de owner para que outra instância não desbloqueie por acidente.
  • Locks em memória. Em serverless, cada execução pode cair numa instância diferente, então flags na memória não servem. Correção: use um armazenamento compartilhado (linha DB, Redis ou KV gerenciado).
  • Assumir "exactly once" sem idempotência. Retries e entrega ao menos uma vez vão te pegar. Correção: escreva com chaves únicas, upserts ou checagem de run-id antes de efeitos colaterais.
  • Usar logs como seu heartbeat. Logs são ótimos para debugar, mas sofríveis para alertas. Correção: escreva um registro de último-run num lugar consultável (DB/KV/métricas).

Uma causa fácil de perder de vista de falha silenciosa é o drift de permissões. O scheduler ainda dispara, mas o worker não consegue mais ler segredos, gravar storage ou chamar uma API após uma mudança.

Checklist rápido antes de colocar em produção

Antes de confiar em cron jobs serverless em produção, faça uma checagem focada em falhas chatas: um scheduler desligado, um lock que expira cedo demais ou um heartbeat que ninguém observa.

  • Confirme que a agenda está habilitada no ambiente certo e que a identidade de runtime pode ler segredos, gravar no DB/fila e emitir logs.
  • Anote sua guarda de concorrência: formato da chave de lock, onde é armazenado, TTL e o que acontece em conflito (pular, reagendar ou falhar).
  • Valide o TTL com tempos reais. Se o job às vezes leva 12 minutos, um lock de 10 minutos vai criar sobreposições.
  • Armazene um heartbeat de "último sucesso" em lugar consultável e inclua status (não apenas timestamp).
  • Faça dois testes intencionais: (1) force uma falha para confirmar que alertas chegam a um humano e (2) force uma sobreposição para confirmar que a segunda execução é bloqueada e logada claramente.

Um teste simples de sobreposição: inicie uma execução com um sleep intencional no meio e, então, dispare uma segunda execução. Se não ver uma mensagem clara "lock held, exiting", sua guarda ainda não é confiável.

Exemplo: um job noturno que nunca deve rodar duas vezes

Harden segurança dos jobs
Corrigimos problemas como SQL injection e logging inseguro em código de jobs serverless.

Um caso doloroso (e comum): um "export de relatórios" noturno roda às 02:00, gera PDFs e envia por e-mail aos clientes. Após um deploy, o scheduler dispara duas vezes (ou um retry entra) e alguns clientes recebem e-mails duplicados. Nada aparenta estar "down", mas a confiança cai rápido.

A correção são duas peças pequenas: um lock para prevenir sobreposição e um heartbeat para pegar paradas silenciosas.

Primeiro, o job pega um lock distribuído (por exemplo, uma linha no banco ou uma chave em um store gerenciado) com TTL maior que o tempo esperado de execução. Se o lock já estiver preso, a segunda invocação sai antes de enviar qualquer coisa.

Fluxo prático:

  • Tentar adquirir lock "nightly-export" com TTL de 45 minutos
  • Se o lock existir, logar "skipped: already running" e parar
  • Se adquirir, gerar o export e enviar e-mails
  • Liberar o lock (TTL é rede de segurança, não o plano)

Segundo, escreva um heartbeat como last_success_at após os e-mails serem enviados. Então rode uma checagem separada a cada 15 minutos que alerte se now - last_success_at for maior que 24 horas + um intervalo. Isso pega o problema de "job parou após deploy" rapidamente.

Para um dono não técnico, os melhores logs e alertas são em linguagem simples:

02:00:01 lock_acquired job=nightly-export run_id=abc123
02:07:44 completed job=nightly-export emails_sent=418 last_success_at=2026-01-20T02:07:44Z
02:00:02 skipped job=nightly-export reason=lock_held current_owner=abc123
ALERT: Nightly export has not succeeded in 25h. Last success: 2026-01-19 02:06 UTC. Check scheduler + secrets.

Próximos passos se seus jobs agendados ainda forem instáveis

Se seus jobs ainda sobrepõem ou "simplesmente param" depois de adicionar lock e heartbeat, pode ser que o agendador esteja ok mas a lógica do job seja frágil.

Um sinal comum é quando a lógica cron veio de um protótipo gerado por IA. Você costuma ver um lock que não é realmente compartilhado, segredos expostos em logs e comportamento de retry que parece útil mas causa efeitos colaterais duplicados.

Sinais de que você está na fase de "para de remendar":

  • Correções funcionam até o próximo deploy, então as falhas mudam de forma
  • Ninguém consegue explicar exatamente quando uma execução é considerada "concluída"
  • Retries geram e-mails duplicados, cobranças ou escritas
  • Auth quebra de forma imprevisível (tokens expirados, refresh faltando, roles incorretas)
  • Logs não permitem reconstruir uma execução ponta a ponta

Nesse ponto, um pequeno passo de remediação costuma ser melhor que ajustes contínuos. O objetivo não é reescrever. É tornar o job previsível: uma entrada clara, uma estratégia de lock, um conjunto de timeouts e um lugar onde o sucesso é registrado.

Se você está lidando com um codebase gerado por IA problemático (especialmente de ferramentas como Lovable, Bolt, v0, Cursor ou Replit), FixMyMess em fixmymess.ai foca em diagnosticar e reparar problemas como execuções sobrepostas, segredos expostos e lógica de retry frágil para que o job seja confiável em produção.