The crontab format was designed in 1975 and has barely changed since. It's terse, unforgiving, and gives you zero feedback when you get it wrong. But once you understand the five-field structure, the rest falls into place quickly.
The five fields
# minute hour day-of-month month day-of-week command * * * * * /usr/bin/mycommand
From left to right: minute (0-59), hour (0-23), day of month (1-31), month (1-12), day of week (0-7, where both 0 and 7 mean Sunday). Then the command.
A * means "every value." So * * * * * means every minute of every hour of every day. 0 * * * * means the top of every hour (minute 0, every hour). 0 2 * * * means 2:00 AM every day.
Reading the common patterns
*/5 * * * * # every 5 minutes 0 * * * * # every hour, on the hour 0 0 * * * # daily at midnight 0 2 * * * # daily at 2am 0 9 * * 1-5 # 9am on weekdays (Monday=1, Friday=5) 0 0 1 * * # first day of every month at midnight 0 0 * * 0 # every Sunday at midnight
The */5 pattern means "every 5th value" — so in the minute field it fires at 0, 5, 10, 15... 55. In the hour field, */6 would fire at 0, 6, 12, 18.
The 1-5 pattern is a range — Monday through Friday. You can also use lists: 1,3,5 means Monday, Wednesday, Friday.
Special strings
Some crontabs use special strings instead of the five fields:
@reboot # runs once when cron starts (every system boot) @hourly # same as 0 * * * * @daily # same as 0 0 * * * @weekly # same as 0 0 * * 0 @monthly # same as 0 0 1 * * @yearly # same as 0 0 1 1 *
@reboot is the one that confuses people. It runs every time the cron daemon starts, which is every system boot. It's not a one-time setup job — it runs on every reboot.
Environment variables at the top
Many crontabs have variable definitions above the jobs:
SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin MAILTO=admin@yourdomain.com 0 2 * * * /usr/bin/backup.sh
SHELL sets which shell runs the commands. PATH is critical — cron has a minimal PATH by default, which is why scripts that work fine when you run them manually fail silently in cron. If your script calls a command that's not in cron's default PATH, it won't be found.
MAILTO is where cron sends output. If you set MAILTO="" — all output is discarded silently. This is the most common cause of "my cron job isn't doing anything." Set MAILTO to your email address during debugging.
Why scripts fail in cron but work manually
Three reasons:
PATH is different. Cron's default PATH is /usr/bin:/bin. If your script calls python3 or node or any tool installed in /usr/local/bin, cron won't find it. Fix: use absolute paths in your scripts (/usr/bin/python3 not just python3) or set PATH at the top of your crontab.
No home directory. Cron doesn't set $HOME the same way your login shell does. Scripts that reference ~/.config/something may fail.
Output is going nowhere. If your script prints errors but you're not capturing output, you'll never see them. Add >> /var/log/myjob.log 2>&1 to the end of your cron command to capture both stdout and stderr to a log file.
# Debug-friendly cron entry: 0 2 * * * /usr/bin/backup.sh >> /var/log/backup.log 2>&1
Reading a real-world crontab
SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO="" # System maintenance 0 0 * * * flock -n /tmp/cleanup.lock /usr/local/bin/cleanup.sh 15 0 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1 0 2 * * 0 /usr/local/bin/weekly-report.sh # Application jobs */5 * * * * flock -n /tmp/queue.lock /app/bin/process-queue 0 * * * * /app/bin/sync-remote-data >> /var/log/sync.log 2>&1 @reboot /usr/local/bin/start-monitoring.sh
Reading this line by line: cleanup runs at midnight with flock protection. DB backup runs at 12:15 AM (staggered from cleanup) with output logged. Weekly report runs Sunday at 2 AM. Queue processor runs every 5 minutes with flock. Remote sync runs every hour. Monitoring script starts on every boot.
This is a well-structured crontab: staggered schedules, flock on anything that might overlap, output captured to logs.
Visualise it before you change it
The hardest part of editing a crontab is understanding the interactions between existing jobs before adding a new one. A new job at 0 0 * * * might look innocent but collide with three existing midnight jobs.
Paste your crontab -l output to see every job on a timeline — before you make changes that cause problems at 2am.
Open Cron Visualiser →