Todos os artigos
74 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Tutoriais

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.

COVER · Tutoriais

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.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil