Cron Jobs Explained: crontab Syntax, Examples and Troubleshooting
Cron runs commands on a schedule: backups, cleanups, certificate checks, reports. It's simple, but its quirks cause most "my cron job doesn't work" problems.
Edit your crontab
crontab -e # edit your jobs
crontab -l # list them
sudo crontab -u www-data -e # edit another user's crontab
The syntax
ββββββββββ minute (0-59)
β ββββββββ hour (0-23)
β β ββββββ day of month (1-31)
β β β ββββ month (1-12)
β β β β ββ day of week (0-7, 0 and 7 = Sunday)
β β β β β
* * * * * command
| Schedule | Expression |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every day at 03:15 | 15 3 * * * |
| Every Monday at 09:00 | 0 9 * * 1 |
| 1st of every month at midnight | 0 0 1 * * |
| Weekdays at 08:30 | 30 8 * * 1-5 |
| Twice a day (06:00 and 18:00) | 0 6,18 * * * |
| At boot | @reboot |
Shortcuts also exist: @hourly, @daily, @weekly, @monthly. To check an expression, use crontab.guru.
A good cron line
15 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
- Use absolute paths for the command and for files it uses.
- Redirect output (
>> file 2>&1), otherwise it's mailed to the user or silently lost. - Make the script executable:
chmod +x /usr/local/bin/backup.sh.
The #1 reason a job fails: the environment
Cron doesn't load your login profile. PATH is minimal (often just /usr/bin:/bin), so a command that works in your terminal fails with "command not found".
Fix it by setting variables at the top of the crontab:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
MAILTO="" disables the emails. Or just use full paths such as /usr/bin/php in the job.
Test what cron sees by simulating its bare environment:
env -i SHELL=/bin/sh PATH=/usr/bin:/bin /usr/local/bin/backup.sh
The % gotcha
In a crontab, an unescaped % means newline. A date command breaks:
# wrong: everything after the first % is cut off
0 2 * * * tar czf /backup/site-$(date +%F).tar.gz /var/www
# right: escape each % ...
0 2 * * * tar czf /backup/site-$(date +\%F).tar.gz /var/www
Or put the logic in a script file, where % needs no escaping. That's the cleaner option.
Prevent overlapping runs
If a job takes longer than its interval, copies pile up. Use flock:
*/5 * * * * flock -n /var/lock/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1
-n makes it skip the run if the previous one still holds the lock.
System-wide jobs
Files in /etc/cron.d/ use the same syntax with an extra user column:
# /etc/cron.d/backup
15 3 * * * root /usr/local/sbin/server-backup >> /var/log/server-backup.log 2>&1
Scripts dropped into /etc/cron.daily/, /etc/cron.weekly/ and similar run automatically. They must be executable and the filename must contain no dot (so backup.sh is ignored, use backup).
Did it run? Check the logs
journalctl -u cron --since today # Debian/Ubuntu
grep CRON /var/log/syslog | tail # older style
sudo tail /var/log/cron # RHEL family
The log shows when a job started, not whether it succeeded, so your own log file (via the redirect) is the real source of truth.
More gotchas
- Time zone: cron uses the server's time zone. Check with
timedatectl. Around daylight-saving changes, a job at 02:30 may run twice or be skipped. Servers are happiest on UTC. - Day-of-month and day-of-week: if you set both, the job runs when either matches.
- Permissions: the job runs as the crontab's owner. A file that user can't read or write causes silent failures.
- No terminal: don't prompt for input or use tools that need a TTY.
- Missed jobs: cron doesn't catch up if the machine was off. Use
anacronor a systemd timer withPersistent=truefor laptops and desktops.
Alternative: systemd timers
For anything that needs logging, dependencies, randomized delays or catch-up after downtime, systemd timers are a good fit. List them with:
systemctl list-timers
For simple scheduled scripts on a server, cron is still perfectly fine.
Monitoring tip
A job that silently stopped working is worse than one that fails loudly. Have the script ping a monitoring URL (like healthchecks.io) when it succeeds, so you're alerted when it doesn't.