Cron and Scheduled Tasks
Two ways to run something on a schedule on Linux - the traditional cron daemon with its terse five-field syntax, and systemd timers as the newer systemd-native alternative.
Both mechanisms solve the same problem - run this command at this time, repeatedly, without a human triggering it - but differ in how much infrastructure they need and what extra features come with them.
Cron
Each user can have their own crontab (cron table):
crontab -e # edit your own crontab
crontab -l # list it
sudo crontab -e -u www-data # edit another user's, as root
Each line has five time fields, then the command:
# minute hour day-of-month month day-of-week command
30 2 * * * /usr/local/bin/backup.sh
0 */4 * * * /usr/local/bin/check-disk.sh
0 9 * * 1-5 /usr/local/bin/weekday-report.sh
| Field | Range | * means |
|---|---|---|
| minute | 0–59 | every minute |
| hour | 0–23 | every hour |
| day of month | 1–31 | every day |
| month | 1–12 | every month |
| day of week | 0–7 (0 and 7 both = Sunday) | every day of the week |
*/4 in the hour field means "every 4 hours"; 1-5 in the day-of-week
field means Monday through Friday. Test unfamiliar syntax at
crontab.guru before deploying it - a single wrong field is easy to miss
and can run something far more or less often than intended.
System-wide cron locations
Beyond individual users' crontabs:
/etc/crontab # system crontab - includes a username field crontab -e doesn't have
/etc/cron.d/* # drop-in system crontabs, same format as /etc/crontab
/etc/cron.daily/* # scripts run once a day (no schedule needed - just drop a script in)
/etc/cron.hourly/*
/etc/cron.weekly/*
/etc/cron.monthly/*
The cron.daily/hourly/etc. directories are the simplest option for
"run this script periodically" when the exact time doesn't matter - no
crontab syntax needed, just an executable script in the right directory.
The classic gotcha: cron's environment is minimal
A script that works fine run manually can fail silently under cron,
almost always because cron jobs run with a much sparser $PATH and no
inherited shell environment - see
Environment Variables. Fixes:
# in the script itself
export PATH=/usr/local/bin:/usr/bin:/bin
# or, use absolute paths throughout
0 2 * * * /usr/bin/python3 /home/alice/scripts/backup.py
Always redirect a cron job's output somewhere you'll actually see it - cron mails output to the crontab's owner by default, which is easy to miss if local mail delivery isn't configured at all:
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
systemd timers: the modern alternative
A timer unit pairs with a service unit (see systemd and Services) to schedule it:
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Service]
ExecStart=/usr/local/bin/backup.sh
sudo systemctl enable --now backup.timer
systemctl list-timers
Persistent=true catches up a missed run if the machine was off at the
scheduled time - something cron has no equivalent for. Timer output goes
to journald automatically (see
Logs and journald), no manual
redirection needed, and timers can express dependencies on other units,
which a plain cron line can't.
Which to use
Cron remains simpler for a quick, single-purpose schedule and is universally available regardless of init system. systemd timers are worth the extra setup when you want dependency ordering, automatic logging, or catch-up-on-missed-runs behavior - increasingly the default choice on systemd-based distributions for anything beyond a trivial script.