Cron jobs and timers
The classic support ticket: “the script works when I run it, but the cron job does nothing.” There are four usual causes and they are all the same cause.
Why it works by hand and not on schedule
Cron gives your job an almost empty environment. Specifically:
- PATH is minimal. Usually just
/usr/bin:/bin. Use absolute paths for everything, including the interpreter. - No shell profile. Nothing from
.bashrcor.profileis loaded, so your environment variables are gone. - The working directory is home, not where your script lives.
cdfirst, or use absolute paths throughout. - A percent sign means newline in a crontab line. Escape it as
\%— this bites every time somebody usesdate +%Y-%m-%d.
Prove it to yourself by capturing the output:
*/5 * * * * /srv/app/job.sh >> /var/log/job.log 2>&1
Without that redirect, cron mails the output to the local user and you will never see it.
Reading the schedule
Five fields: minute, hour, day of month, month, day of week.
0 3 * * *— every day at 03:00*/15 * * * *— every fifteen minutes0 4 * * 1— Mondays at 04:000 2 1 * *— the first of the month at 02:00
Do not schedule everything at exactly midnight. Every machine in the world runs its maintenance then; pick an odd minute and spread your own jobs out.
systemd timers, and why they are usually better
A timer pairs with a service unit, which means you get journald logging, dependency
ordering, resource limits and the sandboxing from
running your application as a service. Create
backup.service with Type=oneshot, then
backup.timer:
[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Persistent=true
Two properties cron does not have: RandomizedDelaySec spreads load
automatically, and Persistent=true runs a missed job after the machine
comes back up instead of silently skipping it.
systemctl list-timers shows every timer with its last and next run.
There is no equivalent for cron.
Make failures visible
A job that fails silently is worse than no job. Three approaches, cheapest first:
- Exit codes and journald. With a timer,
systemctl --failedlists anything that did not finish cleanly. - A dead man's switch. Have the job call a monitoring URL on success; you get alerted when the call stops arriving. This catches the case where the job never ran at all.
- Lock files. Use
flockso a long run does not overlap with the next start:flock -n /var/lock/job.lock /srv/app/job.sh
Check the clock
Schedules depend on the machine agreeing with everyone else about the time. See time and NTP — and be aware of what your timezone does to a job scheduled at 02:30 on the night the clocks change.
Still stuck? Mail support@novogara.com — an engineer answers, at any hour. Back to the knowledge base