Scheduling Tasks with Cron
At a glance#
- Purpose: Schedule recurring jobs on Linux with cron, and diagnose jobs that do not run.
- Applies to: Any Linux distribution.
- Risk: Medium — a badly written job can fill a disk, hammer a database, or run destructive commands unattended.
- Time: 20 minutes.
Overview#
Cron runs commands on a schedule. It is simple, universally available, and the source of a specific family of frustrating faults — almost all of which come from the same root cause: cron does not run your shell environment.
Jobs work perfectly when tested by hand and then fail silently at 2am because PATH is minimal, no profile is sourced, and nobody is reading the output.
Crontab basics#
# edit your own crontab
crontab -e
# list it
crontab -l
# edit another user's (as root)
sudo crontab -e -u www-data
# remove it entirely - no confirmation prompt
crontab -rWarning:crontab -rdeletes the crontab immediately with no confirmation, and it sits next to-eon the keyboard. Back up first:crontab -l > ~/crontab-backup-$(date +%F).txt
Schedule syntax#
* * * * * command
│ │ │ │ │
│ │ │ │ └── day of week (0-7, 0 and 7 both = Sunday)
│ │ │ └──── month (1-12)
│ │ └────── day of month (1-31)
│ └──────── hour (0-23)
└────────── minute (0-59)Common patterns:
| Schedule | Meaning |
|---|---|
0 2 * * * | Every day at 02:00 |
*/15 * * * * | Every 15 minutes |
0 */4 * * * | Every 4 hours |
30 3 * * 0 | Sundays at 03:30 |
0 0 1 * * | First of the month at midnight |
0 9-17 * * 1-5 | Hourly, 09:00–17:00, weekdays |
@reboot | Once at boot |
@daily | Equivalent to 0 0 * * * |
Note: Day-of-month and day-of-week are OR, not AND. 0 0 13 * 5 runs on the 13th and every Friday, not only Friday the 13th. This surprises people.
Writing a job that actually works#
A crontab that avoids the common failures:
# environment - cron gives you almost nothing by default
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=sysadmins@example.com
# nightly backup, all output captured
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# clear old temp files weekly
30 3 * * 0 find /tmp -type f -mtime +7 -delete >> /var/log/tmpclean.log 2>&1
# certificate renewal check twice daily, at odd minutes
17 3,15 * * * /usr/bin/certbot renew --quiet >> /var/log/certbot.log 2>&1Rules worth following every time:
- Use absolute paths for both the command and every file it touches.
- Redirect output with
>> file 2>&1. Without it, output goes to mail — and if mail is not configured, it is discarded and you learn nothing. - Set
PATHexplicitly at the top of the crontab. - Escape
%as\%. In crontab, an unescaped%becomes a newline, which silently truncates the command. This bites anyone usingdate +%F. - Avoid round numbers. Everything scheduled at
0 0runs simultaneously across the estate. Offsetting to17spreads the load.
The percent-sign trap#
# WRONG - breaks at the first %
0 2 * * * tar -czf /backup/db-$(date +%F).tar.gz /var/lib/mysql
# RIGHT - escaped
0 2 * * * tar -czf /backup/db-$(date +\%F).tar.gz /var/lib/mysql
# BETTER - put it in a script and avoid the issue entirely
0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1Putting anything non-trivial in a script is the right answer. It is testable, version-controllable, and free of crontab escaping rules.
System-wide cron#
/etc/crontab # system crontab - has an extra user field
/etc/cron.d/ # drop-in files, same format
/etc/cron.hourly/ # scripts run hourly
/etc/cron.daily/ # scripts run daily
/etc/cron.weekly/
/etc/cron.monthly/System crontab entries include a user column:
# m h dom mon dow user command
0 2 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1sudo nano /etc/cron.d/myapp
sudo chmod 644 /etc/cron.d/myappWarning: Files in/etc/cron.d/must not have a file extension. Cron ignores anything containing a dot, somyapp.confis silently skipped — a fault with no error message anywhere.
Scripts in cron.daily and similar must be executable and also have no extension:
sudo chmod +x /etc/cron.daily/myjobPreventing overlapping runs#
A job that takes longer than its interval will start again before the previous run finishes. flock prevents that:
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1-n exits immediately if the lock is held, so the run is skipped rather than queued.
Checking that jobs ran#
# RHEL family
sudo tail -50 /var/log/cron
sudo grep CRON /var/log/messages
# Debian / Ubuntu
sudo grep CRON /var/log/syslog
sudo journalctl -u cron --since today
# all crontabs on the system
sudo ls -la /var/spool/cron/ # RHEL
sudo ls -la /var/spool/cron/crontabs/ # DebianCron logs that it started a job. It does not log whether the job succeeded — that is why redirecting output to a log file matters.
systemd timers as an alternative#
For anything important, systemd timers are better: real logging, dependency handling, and missed-run recovery. See Managing Services with systemd.
| cron | systemd timer | |
|---|---|---|
| Setup | One line | Two unit files |
| Logging | Whatever you redirect | Automatic, in the journal |
| Missed runs | Lost | Persistent=true catches up |
| Dependencies | None | Full systemd ordering |
| Availability | Everywhere | systemd systems only |
Use cron for simple, low-stakes jobs; timers for anything whose failure matters.
Verification#
# job is registered
crontab -l
sudo cat /etc/cron.d/myapp
# script runs correctly by hand, as the right user
sudo -u www-data /usr/local/bin/myjob.sh
# simulate cron's minimal environment - this is the real test
env -i /bin/bash --noprofile --norc -c '/usr/local/bin/myjob.sh'
# confirm cron started it
sudo grep myjob /var/log/cron
# confirm the job's own log shows success
tail -20 /var/log/myjob.logThe env -i test is the one that catches the classic failure. A script that works in your shell but fails under env -i will fail under cron for exactly the same reason.
Troubleshooting#
| Symptom | Cause and fix |
|---|---|
| Job never runs | Cron service stopped. systemctl status crond (RHEL) or cron (Debian). |
| Works manually, fails in cron | Environment. Use absolute paths and set PATH at the top of the crontab. |
command not found in the log | PATH too minimal. Use the full path to the binary. |
Job truncated at a % | Unescaped percent sign. Use \% or move it into a script. |
File in /etc/cron.d ignored | Filename contains a dot, or permissions are not 644. |
Script in cron.daily ignored | Not executable, or has a file extension. |
| Runs at the wrong time | Server timezone. Check timedatectl; cron uses system local time. |
| Multiple copies running | Job overruns its interval. Wrap with flock. |
| No output anywhere | Not redirected, and no local mail. Add >> log 2>&1. |
| Ran but did nothing | Check the job's own log, not the cron log. Cron only records that it started. |