20.1 Crontab Syntax: Minute, Hour, Day, Month, Weekday
Alright, let’s get our hands dirty with crontab syntax. This is where the magic—and the absolute head-scratching frustration—happens. Forget the pretty GUIs; this is the real control panel. A crontab is simply the file where you define your schedule of jobs (or ‘cron jobs’) for the cron daemon to execute. Each user on a system can have their own crontab, and there’s also a system-wide one (usually /etc/crontab or in /etc/cron.d/).
The syntax looks deceptively simple. It’s just five fields, followed by the command to run. But those five fields are a minefield of off-by-one errors and mistaken assumptions. Here’s the canonical layout:
# m h dom mon dow command
That’s:
m: Minute (0-59)h: Hour (0-23)dom: Day of the month (1-31)mon: Month (1-12)dow: Day of the week (0-7, where both 0 and 7 are Sunday)
Yes, you read that right. The day of the week starts at 0 (Sunday) and goes to 6 (Saturday), and some systems, in a fit of generosity, also accept 7 as Sunday. Because why not? Consistency is the hobgoblin of little minds, apparently.
The Five (or Six) Fields of Dreams (or Nightmares)
Let’s break down each field with the cold, hard truth.
Minute (m): 0-59. Straightforward. * means “every minute,” which is a fantastic way to bring your system to its knees if your command is the least bit expensive.
Hour (h): 0-23. Military time. 0 is midnight. 13 is 1 PM. Get used to it. */2 means “every 2 hours,” which runs at 0, 2, 4, …, 22. It does not mean “every two hours from now.”
Day of the month (dom): 1-31. Here be dragons. What happens if you schedule something for the 31st and the month only has 30 days? The job simply won’t run that month. cron is pedantic, not clever.
Month (mon): 1-12. You can also use names like JAN, FEB, etc., but I don’t trust it. Numbers are universal. Stick to them.
Day of the week (dow): 0-7 (0/7=Sun, 1=Mon, …, 6=Sat). This is the field that causes the most confusion because it has an awkward relationship with dom. Crucially, both dom and dow are evaluated with an OR logic, not AND. If you specify both, the job will run when either condition is true. This is almost never what anyone actually wants.
Operators: The Real Power
The fields aren’t just numbers. You can use operators to build complex schedules.
- Comma (
,): For a list of values.0,15,30,45in the minute field. - Dash (
-): For a range of values.9-17in the hour field for “9 AM to 5 PM.” - Asterisk (
*): For all possible values. - Slash (
/): For step values. This is the most powerful and most misused one.*/5in the minute field means “every 5 minutes” (0, 5, 10…).20-50/10means 20, 30, 40, 50.
Let’s look at some real, runnable examples. Open your crontab with crontab -e and add lines like these:
# Every day at 3:15 AM. The classic backup time.
15 3 * * * /path/to/backup-script.sh
# Every Monday at 9:30 AM. A rude awakening for your weekly report.
30 9 * * 1 /usr/bin/generate-weekly-report
# Every 10 minutes during business hours, Mon-Fri. Perfect for a monitoring script.
*/10 9-17 * * 1-5 /opt/monitoring/check-status.sh
# The 1st and 15th of every month at midnight. For those fortnightly tasks.
0 0 1,15 * * /usr/local/bin/invoice-cron
# Don't do this. This runs at 4:30 AM on the 1st AND every Saturday.
30 4 1 * 6 /path/to/confusing-job.sh
# Instead, to run only on the 1st if it's a Saturday, use this ugly but effective workaround:
30 4 1 * 6 [ "$(date '+\%a')" = "Sat" ] && /path/to/actually-correct-job.sh
Note: The escaped \% is needed in some crontabs to prevent interpreting % as a special character.
The Environment: It’s Barren in Here
This is the single most common pitfall, and it has broken more cron jobs than all syntax errors combined. The environment cron runs your job in is not your lush, comfortable login shell. It’s a stripped-down, minimalist, almost barren environment. It doesn’t have your $PATH, your $HOME, or your aliases.
Your command might work perfectly when you run it in your terminal and fail miserably in cron. Always, always use absolute paths to binaries and scripts.
# This will almost certainly fail
* * * * * my_cool_script.sh
# Do this instead. Use `which my_cool_script` to find the full path.
* * * * * /usr/local/bin/my_cool_script.sh
Also, if your script needs specific environment variables, set them at the top of your crontab or, better yet, inside the script itself.
Output and Logging: The Void
By default, any output (stdout or stderr) from your cron job is emailed to the user who owns the crontab. If your system isn’t set up to send email (which most aren’t by default), this output just… disappears into the void.
You must manually redirect output if you want to capture it. The best practice is to redirect both streams to a log file.
# Redirect all output (stdout and stderr) to a file, appending each run.
*/5 * * * * /path/to/script.sh >> /var/log/myscript.log 2>&1
# Redirect all output to the bitbucket (/dev/null) if you don't care about it.
*/5 * * * * /path/to/script.sh > /dev/null 2>&1
Never let a cron job output to the terminal unchecked. An erroring job that spams stderr can quickly fill up your disk with undeliverable mail files. I’ve seen it happen. It’s not pretty.
The final word of advice? Test your timing with a tool like crontab.guru before you commit. It’s the brilliant friend you wish cron itself had.