Desenvolvimento

Expressões cron explicadas (e a armadilha do OU)

Cinco números e uns símbolos: cron parece a coisa mais simples da administração de sistemas. E é, até a expressão que deveria rodar "toda sexta-feira 13" começar a disparar em todo dia 13 e em toda sexta. Essa não é a única cilada, mas é a que mais gente pisa, porque contraria a leitura intuitiva da esquerda para a direita. Este guia abre os cinco campos, define cada operador com um exemplo, mostra por que */5 não significa "a cada 5 minutos a partir de agora" e separa o cron POSIX das extensões do Quartz (segundos, ?, L, W, #) que você encontra copiando expressões da internet. Monte e teste as suas no [Gerador de Cron](tool:cron-generator) enquanto lê, ele mostra os próximos disparos, que é onde a armadilha do OU aparece na cara.

J-Kit14 min de leituraIntermediário
  • Cron
  • Agendamento
  • Linux
  • Desenvolvimento

Resumo rápido

  • São cinco campos: minuto (0–59), hora (0–23), dia-do-mês (1–31), mês (1–12) e dia-da-semana (0–7, com 0 e 7 = domingo no cron Vixie).
  • Quando dia-do-mês E dia-da-semana são restritos, o cron dispara quando QUALQUER UM casa (OU lógico), não quando ambos casam.
  • */5 são os minutos divisíveis por 5 (0,5,10…), não "5 minutos após instalar"; o passo conta sempre a partir do início do intervalo.
  • Segundos, ?, L, W e # são extensões do Quartz/Spring, não POSIX, não cole essas expressões num crontab de Linux.

Os cinco campos

Um crontab é uma tabela de agendamentos lida pelo daemon cron (na prática, o crond). Cada linha tem cinco campos de tempo seguidos do comando a executar. Os campos são, na ordem: minuto, hora, dia-do-mês, mês e dia-da-semana. O daemon acorda a cada minuto, compara o relógio com cada linha e roda o comando quando os campos casam. A especificação base é a do POSIX (The Open Group), mas quase todo Linux roda uma variante de Vixie cron (hoje mantida como cronie), que acrescenta extensões úteis, os atalhos @daily, os nomes de mês e o valor 7 para domingo.

 ┌───────────── minute        (0-59)
 │ ┌─────────── hour          (0-23)
 │ │ ┌───────── day of month  (1-31)
 │ │ │ ┌─────── month         (1-12)
 │ │ │ │ ┌───── day of week   (0-7, 0/7 = Sun)
 │ │ │ │ │
 * * * * *   command to run
O diagrama clássico dos cinco campos. Um asterisco significa "qualquer valor" naquele campo.
Os cinco campos, seus intervalos e os nomes especiais aceitos pelo Vixie cron.
CampoIntervaloValores/nomes especiais
Minuto0–59
Hora0–23
Dia do mês1–31
Mês1–12nomes JAN–DEC
Dia da semana0–70 e 7 = domingo; nomes SUN–SAT

O dia-da-semana é a pegadinha silenciosa. No POSIX estrito, o intervalo é 0–6 com 0 = domingo. O Vixie cron estende para 0–7 e faz **tanto 0 quanto 7 significarem domingo**, uma cortesia para quem espera a semana terminar no 7. Isso importa quando você escreve 1-5 (segunda a sexta) ou 6,0 (fim de semana). Os nomes de mês e de dia (JAN, SUN) também são extensão do Vixie, não do POSIX; num sistema minimalista, prefira os números. O Gerador de Cron trata 7 como domingo e valida os intervalos campo a campo, então é um bom lugar para conferir se sun e 0 produzem o mesmo agendamento.

Os operadores e o passo

Cada campo aceita quatro construções. O asterisco casa todos os valores; a vírgula lista valores avulsos; o hífen define um intervalo inclusivo; e a barra aplica um passo, pulando de N em N dentro de um intervalo (ou de um asterisco, que equivale ao intervalo cheio do campo). O POSIX define só *, , e -; o passo / é extensão do Vixie, outra razão para não presumir que toda expressão roda em todo lugar.

Os quatro operadores, com um exemplo e o resultado de cada.
OperadorSignificadoExemploResultado
*todos os valores do campo* * * * *a cada minuto
,lista de valores0 0,12 * * *às 00:00 e às 12:00
-intervalo inclusivo0 9-17 * * *de hora em hora, das 09 às 17
/passo dentro do intervalo*/15 * * * *nos minutos 0, 15, 30, 45

**Exemplo trabalhado.** Tome */15 9-17 * * 1-5, a cada 15 minutos, das 9h às 17h, de segunda a sexta. Aqui só o dia-da-semana está restrito (o dia-do-mês é *), então não há armadilha: é um E limpo entre os campos de tempo e os dias úteis. Partindo de uma quinta-feira, 09/07/2026, às 16:07, os próximos disparos são: 16:15, 16:30, 16:45, 17:00, 17:15, 17:30 e 17:45 (a hora 17 entra no intervalo 9-17). Depois de 17:45, a janela fecha; o próximo é sexta 10/07 às 09:00. O fim de semana é pulado, então após sexta o job só volta na segunda 13/07 às 09:00.

A armadilha do OU: dia-do-mês e dia-da-semana

Aqui está a regra que quebra a intuição de todo mundo. Você lê a expressão da esquerda para a direita e supõe que todos os campos precisam casar ao mesmo tempo, um grande E. É verdade para minuto, hora e mês. Mas dia-do-mês e dia-da-semana têm um tratamento especial: quando **os dois** estão restritos (nenhum é *), o cron dispara quando **qualquer um dos dois** casa. É um OU, não um E. O crontab(5) do Vixie diz, literalmente: "If both fields are restricted (i.e., do not contain the '*' character), the command will be run when either field matches the current time." O POSIX descreve a mesma semântica com outras palavras.

Não há como expressar "sexta-feira 13" num único campo do cron POSIX, justamente por causa do OU. A solução idiomática é agendar para todo dia 13, 0 0 13 * *, e testar o dia-da-semana dentro do próprio comando, por exemplo [ "$(date +\%u)" = "5" ] && /caminho/script. Como só o dia-do-mês fica restrito (o dia-da-semana volta a ser *), a regra do OU não se ativa: o cron dispara todo dia 13 e o test decide se aquele 13 é sexta. A regra prática é simples: se você precisa que os dois campos de dia atuem juntos como E, deixe um deles em * e mova a outra condição para um if no script.

Cole 0 0 13 * 5 e leia a lista de próximos disparos: sextas e dias 13 misturados. Depois compare com 0 0 13 * *.Abrir a ferramenta em página inteira

Atalhos: @daily, @reboot e companhia

No lugar dos cinco campos, o Vixie cron aceita apelidos começados por @. Eles não fazem parte do POSIX, mas estão em praticamente todo Linux e deixam a intenção óbvia. Cada apelido corresponde a uma expressão de cinco campos, exceto o @reboot, que não tem equivalente porque significa "uma vez, quando o cron sobe".

Os atalhos padrão do Vixie cron e a expressão que cada um representa.
AtalhoEquivale aQuando roda
@yearly / @annually0 0 1 1 *1º de janeiro, 00:00
@monthly0 0 1 * *dia 1 de cada mês, 00:00
@weekly0 0 * * 0todo domingo, 00:00
@daily / @midnight0 0 * * *todo dia, 00:00
@hourly0 * * * *toda hora, no minuto 0
@reboot(sem equivalente)uma vez, ao iniciar o cron

Dois detalhes: @midnight é sinônimo de @daily no código do cronie, ainda que o manual atual só documente @daily; e @reboot roda quando o **daemon** sobe (tipicamente no boot), não a cada login, se o serviço reiniciar sozinho, o job dispara de novo. Duas dúvidas que sempre voltam, como o cron lida com o horário de verão e o que exatamente conta como "reboot", merecem um pouco mais de espaço.

Cron e horário de verão (DST): jobs pulados e repetidos

O cron roda no fuso do sistema onde o daemon vive, e as transições de horário de verão mexem no relógio local. O manual do cron(8) descreve um tratamento especial para saltos de menos de 3 horas (o caso do DST). Na primavera, quando o relógio avança e uma hora "some", os jobs que teriam rodado na hora pulada são executados logo após a virada. No outono, quando o relógio recua e uma hora se repete, os jobs que caem na hora repetida não são executados de novo. Isso vale para agendamentos de horário específico; jobs com curinga (ex.: * * * * *) rodam normalmente sobre o novo horário.

Mudanças de mais de 3 horas são tratadas como correção de relógio, e o novo horário passa a valer imediatamente. A lição operacional: para tarefas sensíveis a fuso, agende o daemon em UTC ou evite a janela de transição (algo entre 1h e 3h da manhã, dependendo do país). Se precisa raciocinar sobre esses saltos, o guia de fusos horários, UTC e horário de verão detalha por que "02:30" pode não existir ou existir duas vezes, e o conversor de fuso horário ajuda a alinhar o horário do servidor com o do seu público.

O que exatamente @reboot executa

@reboot dispara uma única vez, quando o serviço do cron é iniciado. Na maioria dos sistemas isso coincide com o boot da máquina, mas não é garantido: se você reiniciar apenas o daemon (por exemplo, systemctl restart cron), os jobs @reboot rodam de novo, sem a máquina ter reiniciado. Também não há hora associada, ele não "espera" um horário; executa assim que o cron sobe e lê o crontab.

Por isso @reboot é frágil como mecanismo de inicialização de serviços. Se o objetivo é subir um processo junto com o sistema, um serviço do systemd (com dependências, reinício automático e ordem de boot) é mais robusto. Reserve @reboot para tarefas idempotentes e baratas, limpar um diretório temporário, reconstruir um cache, em que rodar de novo após um restart do daemon não causa dano.

Nem todo "cron" é POSIX: Quartz e Spring

Boa parte das expressões que circulam na internet não roda num crontab de Linux, são do Quartz, o agendador do mundo Java, ou do formato usado pelo Spring. A pista mais visível é a contagem de campos: o Quartz usa seis (segundos, minuto, hora, dia-do-mês, mês, dia-da-semana) mais um sétimo opcional de ano; o Spring usa seis, com o segundo na frente. Se você contar seis números onde esperava cinco, provavelmente está diante de uma expressão com campo de segundos, colá-la num crontab do sistema faz o cron reclamar ou interpretar tudo errado.

Cron POSIX / Vixie (Linux)

  • 5 campos: minuto, hora, dia-do-mês, mês, dia-da-semana.
  • Dia-da-semana 0–7 (0 e 7 = domingo).
  • Operadores *, ,, - e (no Vixie) /.
  • Dia-do-mês e dia-da-semana restritos = OU.
  • Sem segundos, sem ?, L, W, #.

Quartz / Spring (Java)

  • 6 campos (segundos na frente) + ano opcional no Quartz.
  • Dia-da-semana 1–7 (1 = domingo, 7 = sábado).
  • ? = "sem valor específico" num dos campos de dia.
  • L = último dia; W = dia útil mais próximo.
  • # = n-ésimo dia da semana (6#3 = 3ª sexta).

O Quartz resolve a armadilha do OU de um jeito diferente: ele **proíbe** especificar dia-do-mês e dia-da-semana ao mesmo tempo. Você é obrigado a pôr ? em um dos dois, deixando explícito qual dia manda. A documentação afirma que "you must currently use the '?' character in one of these fields". É por isso que expressões do Quartz costumam ter um ? onde o cron do Linux teria um *, e por isso 6#3 (terceira sexta do mês), que o Quartz expressa num campo só, exige um teste no script quando você está no cron POSIX. Ao migrar entre os dois mundos, reescreva a expressão a partir da intenção, não copie os símbolos: além do campo de segundos e do ?, a numeração do dia-da-semana muda (0 = domingo no Linux, 1 = domingo no Quartz).

Receitas prontas

A maioria dos agendamentos do dia a dia cabe em meia dúzia de padrões. A tabela abaixo lista expressões de cinco campos verificadas, com o que cada uma faz. Todas evitam a armadilha do OU: sempre que os dois campos de dia poderiam colidir, um deles fica em *.

Receitas de cron verificadas. Teste qualquer uma no gerador para ver os próximos disparos.
ExpressãoO que faz
0 0 * * *Todo dia à meia-noite (igual a @daily).
*/15 * * * *A cada 15 minutos (nos minutos 0, 15, 30, 45).
0 9 * * 1-5Às 09:00, de segunda a sexta.
0 */6 * * *A cada 6 horas (00:00, 06:00, 12:00, 18:00).
0 0 1 * *Dia 1 de cada mês, 00:00 (igual a @monthly).
0 0 * * 0Todo domingo à meia-noite (igual a @weekly).
23 0-23/2 * * *No minuto 23 de cada hora par (00:23, 02:23, …, 22:23).
30 3 1 1 *Às 03:30 de 1º de janeiro, todo ano.

Uma última recomendação de higiene: agende sempre com um plano B para o horário. Se o servidor estiver desligado no instante exato, o cron comum simplesmente perde o disparo (o anacron cobre parte disso em desktops). E instrumente o job, um cron que roda mas falha em silêncio é pior que um que não roda. Um padrão barato é o job fazer uma requisição a um endpoint de "pulso" no fim; se o pulso não chega, você recebe alerta. Para interpretar o que o servidor devolve nesse pulso, o guia de códigos de status HTTP essenciais separa o 2xx que confirma sucesso do 5xx que denuncia falha. E quando o job grava carimbos de tempo Unix nos logs, o conversor de timestamp transforma aqueles números em datas legíveis na hora de depurar.

Perguntas frequentes

Por que meu cron de "sexta-feira 13" roda em outros dias?
Porque 0 0 13 * 5 tem os dois campos de dia restritos, e o cron os combina com OU: ele dispara em todo dia 13 OU em toda sexta. Para rodar só na sexta-feira 13, agende 0 0 13 * * e teste o dia-da-semana dentro do script, por exemplo com [ "$(date +\%u)" = "5" ].
*/5 significa "a cada 5 minutos a partir de agora"?
Não. O passo conta a partir do início do intervalo do campo, então */5 casa os minutos 0, 5, 10, …, 55, os divisíveis por 5, independente de quando você instalou o job. Passos que não dividem o intervalo criam espaçamento irregular: */45 nos minutos casa só 0 e 45.
0 ou 7 para domingo, qual usar?
No Vixie cron (a maioria dos Linux), tanto 0 quanto 7 significam domingo, então os dois funcionam. No POSIX estrito o intervalo é 0–6, com só o 0 valendo como domingo. Para portabilidade máxima, prefira 0; e lembre que o Quartz é diferente: lá o domingo é 1.
Por que uma expressão de 6 campos não funciona no meu crontab?
Quase certamente é uma expressão do Quartz ou do Spring, que colocam um campo de segundos na frente (6 campos, mais um de ano no Quartz). O cron do Linux espera exatamente 5 campos e não entende segundos, nem os caracteres ?, L, W e #. Reescreva a partir da intenção, sem o campo de segundos.
O cron respeita o horário de verão?
O cron roda no fuso do sistema. Em saltos de menos de 3 horas (o caso do DST), o cron(8) tem tratamento especial: jobs na hora pulada (primavera) rodam logo após a virada, e jobs na hora repetida (outono) não rodam de novo. Para evitar surpresas, agende o daemon em UTC ou fuja da janela de transição.

Cron são cinco campos e quatro operadores, mas dois detalhes derrubam quem confia na intuição: */n conta a partir do zero do campo, não de "agora", e dia-do-mês com dia-da-semana viram um OU quando os dois estão restritos. Domine esses dois pontos, saiba distinguir o cron POSIX das extensões do Quartz e valide sempre pelos próximos disparos antes de confiar num agendamento.

Fontes e referências

  1. The Open Group, crontab (POSIX Base Specifications)
  2. crontab(5), man page do Vixie/cronie
  3. cron(8), man page (horário de verão / DST)
  4. cronie, código-fonte de crontab(5) e entry.c
  5. Quartz Scheduler, tutorial de CronTrigger (?, L, W, #)