Backups, log rotation, certificate renewal, report generation and cache warming all need to run without anyone typing a command. Linux offers two schedulers: cron, which has worked the same way for decades and is what most documentation assumes, and systemd timers, which integrate with services, logging and dependency handling. This lesson teaches the crontab format precisely, the environment quirks that make cron jobs fail silently, and how to write the equivalent timer.
Each user has a crontab, edited with crontab -e. Every line is five time fields followed by a command.
# ┌───────── minute (0-59)
# │ ┌─────── hour (0-23)
# │ │ ┌───── day of month (1-31)
# │ │ │ ┌─── month (1-12)
# │ │ │ │ ┌─ day of week (0-7, 0 and 7 are Sunday)
# │ │ │ │ │
# * * * * * command| Expression | Runs |
|---|---|
| * * * * * | every minute |
| */5 * * * * | every 5 minutes |
| 0 * * * * | at the top of every hour |
| 30 2 * * * | daily at 02:30 |
| 0 9 * * 1-5 | 09:00 on weekdays |
| 0 0 1 * * | midnight on the first of each month |
| 0 3 * * 0 | 03:00 every Sunday |
| 0 6,18 * * * | 06:00 and 18:00 |
| @reboot | once at boot |
| @daily, @hourly, @weekly | shortcuts for 0 0 * * * etc. |
Day-of-month and day-of-week are ORed when both are restricted: 0 0 13 * 5 runs on the 13th and on every Friday, not only on Friday the 13th.
crontab -e # edit your crontab (opens $EDITOR)
crontab -l # list it
crontab -r # remove it entirely (no confirmation)
sudo crontab -u www-data -e # edit another user's crontab
ls /etc/cron.d/ # system jobs installed by packages
ls /etc/cron.daily/ # scripts run once a day by the system/etc/crontab and files in /etc/cron.d/ have an extra field — the user to run as — between the schedule and the command. Packages such as logrotate and certbot install their jobs there.
Cron runs commands with a minimal environment: no ~/.bashrc, a PATH of just /usr/bin:/bin, and /bin/sh rather than bash. A script that works in your terminal can fail in cron because node, aws or psql are not on that PATH. The fixes:
# At the top of the crontab
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
# Always use absolute paths and capture output
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1Output that is not redirected is emailed to MAILTO (or the user) if a mail system exists, and otherwise discarded. Redirect to a log file so failures leave evidence. Also note that % is special in crontab commands (it means newline); escape it as \% in date +\%F.
Check whether a job ran at all in the system log:
grep CRON /var/log/syslog | tail # Debian/Ubuntu
journalctl -u cron --since today # systemd systemsA job scheduled every five minutes that sometimes takes six will pile up. Wrap it in flock so a second copy exits if the first still holds the lock.
*/5 * * * * flock -n /tmp/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1A timer is a pair of unit files: a .service that describes the work and a .timer that describes when. The service is an ordinary one-shot service, so it gets logging in journalctl, resource limits, and can depend on the network being up.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly database backup
After=network-online.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetPersistent=true runs the job at next boot if the machine was off at the scheduled time, something cron cannot do. RandomizedDelaySec spreads load when many machines share a schedule.
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers # next and last run of every timer
systemctl status backup.timer
sudo systemctl start backup.service # run the job right now, manually
journalctl -u backup.service --since yesterdayThe format is DayOfWeek Year-Month-Day Hour:Minute:Second, with * as a wildcard and / for steps. Check any expression with systemd-analyze calendar.
| OnCalendar | Meaning |
|---|---|
| hourly, daily, weekly | shortcuts |
| *-*-* 02:30:00 | every day at 02:30 |
| Mon..Fri *-*-* 09:00:00 | weekdays at 09:00 |
| *:0/15 | every 15 minutes |
| *-*-01 00:00:00 | first of the month |
| Sun *-*-* 03:00:00 | Sundays at 03:00 |
systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"For "every 10 minutes after boot regardless of clock time", use OnBootSec=5min and OnUnitActiveSec=10min instead of OnCalendar.
PATH set in .bashrc.2>&1, so errors vanish.% in the command./etc/crontab and omitting the user field..service instead of the .timer, which runs it once at boot and never again.Which crontab line runs a job at 07:15 every Monday?
minute hour day month weekday command; */n means every n units and @daily style shortcuts exist.PATH, use absolute paths, redirect output with >> log 2>&1, escape %.flock -n prevents overlapping runs; MAILTO or a log file makes failures visible..timer plus a .service; enable the timer, inspect with systemctl list-timers and journalctl -u.OnCalendar with Persistent=true catches up on missed runs; verify expressions with systemd-analyze calendar.Next lesson: systemd Services and journalctl — run your own applications as managed services and read their logs.