Agendamento — cron e timers
TL;DR
O cron funciona há quarenta anos e falha sempre pelos mesmos três motivos: o ambiente mínimo que ele entrega (o script acha o binário no seu terminal e não acha lá), a saída que vira e-mail que ninguém lê, e o fato de que ele não sabe que a máquina esteve desligada na hora marcada. Os timers do
systemdresolvem os três — a saída vai para o journal da nota 08, a execução é uma unidade com todo o contrato da nota 07, ePersistent=truerecupera o que foi perdido. O preço é dois arquivos em vez de uma linha.
O script que funciona quando você executa e falha às três da manhã
Você escreve o backup, testa à mão, funciona. Agenda no cron. No dia seguinte, nada foi feito — e não há erro em lugar nenhum.
0 3 * * * /opt/scripts/backup.shO script existe, tem permissão de execução, e o horário está certo. O que mudou foi o contrato com que ele rodou — de novo a nota 01, agora do lado do agendador.
Três diferenças explicam quase todos os casos:
O PATH é mínimo. O cron não carrega o seu .bashrc nem o .profile. O PATH que ele entrega costuma ser apenas /usr/bin:/bin. Se o script chama docker, aws, node ou qualquer coisa instalada fora disso, o comando “não existe”.
A saída não vai para lugar nenhum útil. Por padrão, o que o job imprime é enviado por e-mail local ao dono da tarefa — em servidor sem MTA configurado, isso significa descartado. O erro aconteceu, foi relatado, e ninguém leu.
O ambiente é praticamente vazio. Sem HOME como você espera, sem variáveis de aplicação, sem as credenciais que o seu shell exporta.
A correção clássica é declarar tudo explicitamente e capturar a saída:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1Repare no >> ... 2>&1, na ordem que a nota 03 explicou — e note que isso é, no fundo, reimplementar log à mão, que é justamente o que a segunda metade desta nota torna desnecessário.
Cron: o que ainda vale saber
crontab -e # o crontab do seu usuário
crontab -l # listar
sudo crontab -e -u www-data # o de outro usuárioE os arquivos de sistema, que têm um campo a mais — o usuário sob o qual rodar:
# /etc/cron.d/backup (arquivo de sistema)
0 3 * * * backup /opt/scripts/backup.sh
# crontab de usuário: sem o campo de usuário
0 3 * * * /opt/scripts/backup.shConfundir os dois formatos é erro comum: colar uma linha de sistema no crontab pessoal faz o cron interpretar o nome do usuário como o comando.
A sintaxe dos cinco campos:
┌───── minuto (0-59)
│ ┌─── hora (0-23)
│ │ ┌─ dia do mês (1-31)
│ │ │ ┌─ mês (1-12)
│ │ │ │ ┌─ dia da semana (0-7, 0 e 7 = domingo)
* * * * **/15 a cada quinze, 1-5 intervalo, 1,15 lista. E os atalhos @daily, @hourly, @reboot.
Dia do mês e dia da semana são um OU, não um E
0 3 1 * 1não significa “no dia 1, se for segunda”. Quando os dois campos estão preenchidos, o cron dispara se qualquer um casar — ou seja, todo dia 1 e toda segunda-feira. É contraintuitivo e está no comportamento padrão há décadas. Quando precisar da conjunção, deixe um dos campos como*e faça a verificação dentro do script.
Timers: a mesma ideia como unidade
Um timer são dois arquivos: o que faz e o que agenda.
# /etc/systemd/system/backup.service
[Unit]
Description=Backup diário
[Service]
Type=oneshot
User=backup
WorkingDirectory=/opt/scripts
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/opt/scripts/backup.sh# /etc/systemd/system/backup.timer
[Unit]
Description=Dispara o backup diário
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=5m
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers # próximas execuções, e quando foi a últimaRepare no que veio de graça: o .service é uma unidade comum, então ele tem todo o contrato da nota 07 — usuário, diretório de trabalho, ambiente, limites, endurecimento. O problema do PATH deixa de ser folclore e vira um campo declarado.
As diretivas de agendamento
| Diretiva | Dispara |
|---|---|
OnCalendar= | em horário absoluto — o equivalente ao cron |
OnBootSec= | tanto tempo depois do boot |
OnUnitActiveSec= | tanto tempo depois da última execução |
OnActiveSec= | tanto tempo depois de o timer ser ativado |
E as três que resolvem problemas reais:
Persistent=true — se a máquina estava desligada na hora marcada, executa assim que ligar. É a resposta ao terceiro defeito do cron, e é decisiva em laptop e em máquina que não fica no ar o tempo todo.
RandomizedDelaySec= — espalha a execução dentro de uma janela. Quando cinquenta máquinas rodam o mesmo backup às três em ponto, elas batem no mesmo servidor no mesmo segundo; um atraso aleatório resolve sem coordenação.
AccuracySec= — a folga que o systemd pode usar para agrupar despertares e poupar energia. O padrão é um minuto, o que surpreende quem espera precisão de segundo; declare AccuracySec=1s se o horário exato importar.
A sintaxe do OnCalendar é mais legível que a do cron, e — melhor ainda — é testável:
systemd-analyze calendar "*-*-* 03:00:00"
systemd-analyze calendar "Mon *-*-* 09:00:00" --iterations=5Isso mostra as próximas ocorrências. Não existe equivalente no cron, onde a única forma de conferir a expressão é esperar.
Vídeo — os dois arquivos, montados do zero
Automate Your Tasks with systemd Timers (Learn Linux TV, ~33 min, EN) constrói o par
.service+.timerdo começo, com um exemplo pequeno e verificável. O ponto que ele demonstra e que mais confunde quem vem do cron aparece por volta de [19:39]: o serviço fica desabilitado e parado de propósito — quem se habilita é o timer, e é ele quem dispara o serviço na hora marcada. Versystemctl statusmostrando o serviço inativo enquanto o agendamento funciona é o que faz a separação entre “o que faz” e “o que agenda” parar de parecer burocracia. Ele também percorre a sintaxe doOnCalendarvariando o mesmo exemplo — data absoluta, todo dia, de hora em hora — e menciona recursos que o cron não tem, como reexecutar trabalho perdido e ajustar prioridade. O que ele não cobre:Persistent=truecom a ênfase que esta nota dá,RandomizedDelaySec=,AccuracySec=, e a comparação direta com o cron nos três defeitos.⚠️ O vídeo tem segmento patrocinado no início (provedor de nuvem). O conteúdo técnico não depende disso, mas vale saber antes de recomendar a alguém.
Por que o timer venceu
graph TB A["<b>cron</b>"] --> A1["ambiente mínimo"] A --> A2["saída vira e-mail<br/>ou desaparece"] A --> A3["perdeu a janela?<br/>não roda"] A --> A4["sem limite de recurso"] B["<b>timer</b>"] --> B1["contrato completo<br/>da unidade"] B --> B2["saída no journal"] B --> B3["Persistent=true<br/>recupera"] B --> B4["cgroup da unidade"]
Vale destacar a segunda linha, porque é a que muda o dia a dia: a saída do job vai para o journal, com os campos confiáveis da nota 08. Investigar uma execução passada é journalctl -u backup.service --since yesterday — não é procurar um arquivo que talvez exista.
E há uma vantagem que só aparece na hora do aperto: dá para executar o trabalho sem esperar o horário, porque ele é uma unidade.
systemctl start backup.service # roda agora, exatamente como rodaria às 3h
journalctl -u backup.service -n 50Testar um job de cron significa reproduzir o ambiente dele à mão, o que quase ninguém faz corretamente — e é por isso que tantos jobs “funcionam no teste”.
Quando o cron ainda é a escolha certa
Máquina sem
systemd(Alpine, muitos containers, Unix não-Linux), contêiner enxuto onde subirsystemdnão faz sentido, e sistemas antigos que você não vai modernizar. Também há o caso simples e legítimo: uma tarefa pessoal, numa máquina só, onde uma linha resolve e dois arquivos seriam cerimônia. Não é obsoleto — é a opção com menos recursos. Em container, aliás, a resposta melhor costuma ser nenhum dos dois: é umCronJobdo Kubernetes ou o agendador da plataforma, assunto da nota 11 do galho de Kubernetes.
Um exemplo trabalhado: o job que roda duas vezes
O backup começa a demorar mais que o intervalo, e passam a existir duas execuções simultâneas — que corrompem o resultado.
No cron, isso não é impedido. O cron dispara no horário, sem saber se a execução anterior terminou. A solução é um bloqueio explícito:
0 * * * * flock -n /var/lock/backup.lock /opt/scripts/backup.shO flock -n desiste se o bloqueio já estiver tomado, o que é exatamente o desejado.
No timer, o problema não existe da mesma forma. Como o trabalho é uma unidade, o systemd não inicia uma segunda instância enquanto a primeira está ativa — a partida é ignorada e registrada. O comportamento padrão já é o correto.
Esse contraste é o melhor argumento único a favor do timer: o estado da execução é conhecido, porque a execução é um objeto do sistema, não um comando disparado no vazio.
Armadilhas comuns
Supor o
PATHdo seu terminalO que acontece: “command not found” num script que funciona quando você o executa. Por quê: cron entrega ambiente mínimo, sem carregar seus arquivos de perfil. Como evitar: caminho absoluto para todo binário, ou
PATH=declarado no topo do crontab — e, no timer,Environment=PATH=...na unidade.
Deixar a saída ir para o vazio
O que acontece: o job falha por semanas e ninguém percebe. Por quê: sem MTA, o e-mail do cron é descartado; e sucesso silencioso é indistinguível de nunca ter rodado. Como evitar: no cron, redirecione para arquivo. No timer, já vai para o journal. Em qualquer um dos dois, o passo seguinte é monitorar a ausência — um job que deveria rodar e não rodou é um alerta, e isso é disciplina de Operação.
Testar só executando à mão
O que acontece: funciona no teste e falha agendado, pelo ambiente. Por quê: você testou com o seu contrato, não com o do agendador. Como evitar: com timer,
systemctl start <unidade>.serviceroda com o contrato real. Com cron, aproxime-se comenv -i /bin/bash --noprofile --norc -c '/opt/scripts/backup.sh'.
Esquecer o
daemon-reloadao criar timerO que acontece:
enable --nowreclama que a unidade não existe, ou usa uma versão antiga. Por quê: mesma regra da nota 06 — arquivo novo ou alterado exigedaemon-reload. Como evitar:daemon-reloade depoislist-timerspara confirmar que a próxima execução está onde você espera.
Como explicar em inglês
“Cron fails for the same three reasons it always has: a minimal environment, so the script can’t find binaries that exist in your shell; output that goes to local mail nobody reads; and no memory of missed runs when the machine was off. systemd timers fix all three — the job is a regular unit, so it gets the full execution contract, its output lands in the journal, and Persistent=true catches up after downtime. You also get testability: systemd-analyze calendar shows the next occurrences, and you can trigger the service manually to run it exactly as it would run on schedule.”
| PT | EN |
|---|---|
| tarefa agendada | scheduled job |
| janela perdida | missed run |
| execução sobreposta | overlapping run |
| bloqueio (lock) | lock |
| atraso aleatório | randomized delay |
| ambiente mínimo | minimal environment |
O que vem a seguir
Boa parte das tarefas agendadas fala com o mundo: envia backup para um servidor remoto, busca atualização, renova certificado. Quando uma delas falha às três da manhã, a primeira bifurcação é sempre a mesma — foi o agendamento, foi a rede, ou foi a resolução de nomes? A próxima nota trata da máquina como participante de uma rede: quais interfaces existem, para onde as rotas apontam, o que está escutando, e por que a resolução de nomes é a origem de tanta falha intermitente.
- 10 — A máquina na rede — interfaces, rotas, portas e nomes.
- 08 — Logs — para onde vai a saída do trabalho agendado.
- 07 — Escrever um serviço — o contrato que o
.servicedo timer herda inteiro.
Fontes
- Michael Kerrisk — crontab(5) — a sintaxe dos campos e o comportamento de OU entre dia do mês e dia da semana.
- freedesktop.org — systemd.timer(5) —
OnCalendar=,Persistent=,RandomizedDelaySec=eAccuracySec=. - freedesktop.org — systemd.time(7) — a gramática das expressões de calendário, testável com
systemd-analyze calendar. - util-linux — flock(1) — o bloqueio que evita execução sobreposta no cron.