Expressões cron desmistificadas: cinco campos, intervalos, passos e a armadilha OR do dia da semana

Os cinco campos e seus intervalos válidos

Toda expressão cron padrão consiste em exatamente cinco campos separados por espaço em branco, lidos da esquerda para a direita: minuto (0–59), hora (0–23), dia do mês (1–31), mês (1–12) e dia da semana (0–7). O minuto é sempre o campo mais à esquerda — um erro comum é ler uma linha cron da direita para a esquerda, o que inverte completamente o significado. O campo do dia da semana aceita tanto 0 quanto 7 para representar domingo, uma redundância deliberada herdada da tradição Unix.

Em implementações que seguem as convenções do Vixie cron, o campo do mês também aceita abreviações de três letras em inglês (jan a dec) e o dia da semana aceita sun a sat. Para portabilidade entre ambientes, é mais seguro usar valores numéricos. O Quartz Scheduler (usado em aplicações Java) acrescenta um campo de segundos à esquerda, e alguns agendadores em nuvem como o AWS EventBridge adicionam um sétimo campo opcional de ano — esses formatos estendidos não são intercambiáveis com o padrão POSIX de cinco campos.

Curingas, listas por vírgula, intervalos por hífen e passos por barra

Quatro caracteres especiais controlam como um campo é correspondido: o asterisco (*) corresponde a cada valor válido para aquele campo — * * * * * é executado a cada minuto. A vírgula (,) cria uma lista de valores discretos: 1,3,5 no campo do mês significa janeiro, março e maio. O hífen (-) define um intervalo inclusivo: 9-17 no campo de hora corresponde a cada hora de 9 a 17 inclusive — útil para agendamentos 'somente em horário comercial'. Esses três caracteres podem ser combinados: 1-5,0 no campo do dia da semana corresponde de segunda a sexta mais domingo.

A barra (/) introduz um valor de passo. Usada após um asterisco, */15 no campo de minutos produz o conjunto {0, 15, 30, 45} — cada múltiplo de 15 a partir do mínimo do campo. Usada após um intervalo, 0-30/5 produz {0, 5, 10, 15, 20, 25, 30}. O passo sempre começa no limite esquerdo do intervalo (ou em 0 para *), nunca em um deslocamento aleatório. É por isso que */5 no campo de minutos é acionado em 0, 5, 10 … independentemente de quando o daemon cron foi iniciado.

A armadilha OR entre dia do mês e dia da semana

O comportamento mais surpreendente do cron ocorre quando ambos o campo dia do mês e o campo dia da semana são definidos com valores não-curinga. Intuitivamente, 0 2 15 * 5 parece significar 'executar às 2h da manhã somente nas sextas-feiras que caem no dia 15 do mês' — uma condição E. Na prática, a maioria das implementações cron, incluindo Vixie cron e dcron, trata isso como uma condição OU: a tarefa é executada às 2h no dia 15 de cada mês, e também às 2h toda sexta-feira, independentemente da data. A página de manual cron(5) documenta isso explicitamente: 'se tanto DOM quanto DOW forem especificados, o comando será executado quando qualquer um dos campos corresponder à hora atual'.

Esse comportamento pega os desenvolvedores quando tentam expressar condições de data compostas. Se você realmente precisa da 'terceira sexta-feira de cada mês', o cron não pode expressá-lo diretamente. A solução idiomática é agendar para toda sexta-feira (0 2 * * 5) e adicionar um teste de shell no início do comando: [ $(date +\%d) -ge 15 ] && [ $(date +\%d) -le 21 ] && /caminho/para/script. Alternativamente, use um agendador de alto nível que suporte nativamente regras de recorrência de calendário (RFC 5545 RRULE), como as diretivas OnCalendar= dos temporizadores systemd.

Atalhos predefinidos: @reboot, @hourly, @daily, @weekly, @monthly

O Vixie cron introduziu um conjunto de atalhos nomeados que substituem a sintaxe de cinco campos para agendamentos comuns. @reboot executa a tarefa uma vez imediatamente após o daemon cron ser iniciado — útil para tarefas leves de inicialização, embora para serviços de produção seja preferível usar um sistema init adequado (unidade systemd, etc.). @hourly equivale a 0 * * * *. @daily (alias @midnight) equivale a 0 0 * * *; @weekly a 0 0 * * 0 (meia-noite de domingo); @monthly a 0 0 1 * * (meia-noite do primeiro dia de cada mês); e @yearly (alias @annually) a 0 0 1 1 * (meia-noite de 1º de janeiro).

Esses atalhos melhoram a legibilidade dos arquivos crontab, mas dependem do daemon cron reconhecer a sintaxe @ — o POSIX não os define, portanto não estão disponíveis em algumas implementações mínimas ou embarcadas. Ao escrever expressões cron para agendadores em nuvem (AWS EventBridge, Google Cloud Scheduler, GitHub Actions), consulte a documentação do provedor, pois a maioria das plataformas em nuvem não implementa os atalhos @ e requer o formato explícito de cinco campos.

Valores de passo e a armadilha 'não a cada N a partir de agora'

Um equívoco frequente sobre a sintaxe de passo é tratar */N como 'a cada N unidades desde quando esta tarefa foi criada'. Não é assim. O passo é um filtro módulo sobre valores de tempo absolutos. */5 no campo de minutos é uma abreviação para o conjunto {x : x mod 5 = 0, 0 ≤ x ≤ 59} — doze posições fixas de relógio por hora. O daemon cron compara essas posições com o minuto atual do relógio; ele não tem memória de quando sua tarefa foi registrada.

Isso produz um problema sutil com 0 */6 * * *. Essa expressão é acionada nas horas 0, 6, 12 e 18 — meia-noite, 6h, meio-dia e 18h — exatamente quatro vezes por dia. Se você adicionar essa tarefa às 15h esperando 'a primeira execução seis horas depois às 21h', ficará surpreso: a próxima execução é às 18h (hora 18). Se precisar de uma execução às 21h, use a lista explícita 0 9,15,21,3 * * * ou mude a hora de início com 0 3/6 * * * — mas note que a notação INÍCIO/PASSO é uma extensão do Vixie cron; para máxima portabilidade, use sempre a lista explícita separada por vírgulas.

Fusos horários: o bug invisível nos agendamentos cron

Os daemons cron tradicionais executam todas as tarefas no fuso horário do sistema da máquina onde são executados. Em um servidor configurado para UTC — que é o padrão para a maioria das imagens base de contêineres Docker e muitas VMs em nuvem — 0 8 * * 1-5 não significa 8h no seu horário local; significa 8h UTC. Se a sua equipe está em UTC+9 (Horário Padrão do Japão), essa tarefa é acionada às 17h no horário local. As expressões cron do AWS CloudWatch Events (agora EventBridge) são documentadas como apenas UTC. on: schedule: do GitHub Actions também é UTC.

O horário de verão (DST) introduz dois modos de falha adicionais. Quando os relógios adiantam (por exemplo, de 01:59 para 03:00), qualquer tarefa cron agendada durante a hora pulada simplesmente não é executada naquele dia. Quando os relógios atrasam (de 02:59 para 02:00), o mesmo minuto aparece duas vezes; se a tarefa é executada uma ou duas vezes depende da implementação do daemon. A estratégia mais segura em produção é manter os servidores cron permanentemente em UTC e converter para o horário local na camada de aplicação, ou usar um agendador que aceite um parâmetro TimeZone explícito, como temporizadores systemd com TimeZone=, Kubernetes CronJob com .spec.timeZone (adicionado no Kubernetes 1.25), ou AWS EventBridge Scheduler com nomes de fuso IANA.