Três erros de cron expression que só aparecem em produção
Timezone implícito, OR lógico no dia-da-semana e meses sem dia 31 — três erros de cron expression sintaticamente válidos que falham silenciosamente em produção.
O job rodou em staging, rodou no meu Mac, rodou na primeira vez em produção. Aí parou. Não com erro — simplesmente não rodou mais. Era um cron mensal. Levei quatro semanas para perceber.
Esses são os três erros que cometi (ou vi alguém cometer) com cron expressions — todos sintaticamente válidos, todos silenciosos.
Erro 1: timezone implícito
0 8 * * *
Esse job deveria rodar todo dia às 8h. Em qual timezone? Depende de onde está rodando.
No servidor de desenvolvimento: horário local, tudo certo. No container de produção na AWS: UTC. Resultado: o job rodava às 5h no verão e às 4h no inverno, porque o servidor estava em UTC e o time esperava 8h de Brasília.
O cron em si está correto. O problema é assumir que "8h" significa alguma coisa sem definir o contexto. Em sistemas distribuídos com containers, o timezone do servidor quase sempre é UTC — e se você está em BRT (UTC-3), 0 8 * * * em UTC é 0 5 * * * no horário local.
A solução depende da plataforma. No Linux com crontab, você pode definir a variável antes da expressão:
CRON_TZ=America/Sao_Paulo
0 8 * * *
No Kubernetes CronJob:
spec:
schedule: "0 8 * * *"
timeZone: "America/Sao_Paulo"
No GitHub Actions não tem campo nativo de timezone — você escreve em UTC e faz a conta. Se quer rodar às 8h BRT, é 0 11 * * * no verão (BRT = UTC-3) e 0 10 * * * no horário de Brasília de verão (BRST = UTC-2). Escolha um e documente.
Erro 2: o quinto campo não é o que você pensa
0 0 * * 7
Domingo à meia-noite — ou não. Em algumas implementações, 7 é domingo. Em outras, só 0 é domingo, e 7 é inválido ou interpretado como segunda-feira. O padrão POSIX define 0–6 (domingo a sábado). O Vixie cron aceita 0 e 7 para domingo. Mas nem todo cron daemon é o Vixie cron.
O mesmo problema aparece com o campo de mês quando você combina dia-do-mês com dia-da-semana:
0 0 1 * 1
A intenção provavelmente era "todo dia 1 do mês, às segundas". Mas a semântica real é "às segundas OU no dia 1 do mês" — é um OR lógico, não um AND. Se você quer o job apenas quando as duas condições forem verdadeiras, cron não resolve diretamente; você coloca a verificação dentro do script.
Erro 3: jobs mensais com dia inexistente
0 9 31 * *
Fevereiro não tem dia 31. Abril, junho, setembro e novembro também não. Esse job vai pular quatro ou cinco meses por ano sem nenhum aviso. Não vai falhar — vai simplesmente não rodar.
O mesmo vale para 0 9 29 2 * em anos não bissextos.
Se você precisa de execução mensal consistente, 1 é o dia mais seguro — existe em todos os meses. Se precisa do último dia do mês, o cron padrão não tem suporte nativo; você usa 28 (conservador) ou implementa via script:
# Rodar apenas se hoje é o último dia do mês
[ "$(date +%d)" = "$(cal | awk '/[0-9]/{last=$NF} END{print last}')" ] && ./meu-job.sh
Validando antes de subir
Os três erros acima têm em comum que nenhum linter pega — a expressão é válida. O único jeito de confirmar que o job vai rodar quando você espera é ver as próximas N execuções antes de subir em produção.
Para isso uso o Gerador de Expressão Cron — você cola a expressão e vê imediatamente as próximas execuções com data e hora completas. Especialmente útil para jobs mensais, semanais em dias específicos, e qualquer expressão com campos não-triviais. Dez segundos de verificação evitam quatro semanas de "por que esse job não rodou?"
O problema não é a sintaxe
Cron expression é uma das poucas tecnologias onde estar certo sintaticamente não significa estar certo. O parser aceita qualquer coisa entre 0 e 59 no primeiro campo — não tem como ele saber que você queria UTC-3 ou que fevereiro não tem 31 dias.
A disciplina é sempre a mesma: escrever a expressão, confirmar as próximas execuções, e documentar o timezone esperado junto do job. É chato. É o tipo de coisa que você faz uma vez, esquece, e lembra quando o relatório mensal não chegou.
- 01 diff antes de abrir PR: o hábito que reduz comentários de revisão Ler o próprio diff antes de abrir um PR muda o que você vê no código. Como e por que esse hábito reduz ciclos de revisão e melhora a qualidade dos PRs.
- 02 `P@ssw0rd1` passa na sua validação. Esse é o problema. O que o NIST mudou em 2024 sobre senhas: comprimento bate complexidade, leet speak está no rockyou.txt e rotação obrigatória enfraquece senhas.