Technical guide: crontab(5) syntax, step operators and the daylight saving problem
How to read the five fields of a cron expression, avoid the day-of-month versus day-of-week OR trap, and schedule jobs that survive clock changes.
1. The five crontab(5) fields and their exact ranges
A standard crontab line holds five space-separated fields: minute (0-59), hour (0-23), day of month (1-31), month (1-12 or JAN-DEC) and day of week (0-7 or SUN-SAT, where both 0 and 7 mean Sunday in Vixie cron). Read `0 8 * * 1-5` as minute 0, hour 8, any day of month, any month, Monday through Friday. The three-letter names are case-insensitive and, in older implementations, cannot be combined with step values. Crontab also accepts the at-sign shortcuts: `@hourly` expands to `0 * * * *`, `@daily` to `0 0 * * *`, and `@reboot` fires once when the daemon starts.
2. The *, -, comma and slash operators, and the day-of-month OR rule
An asterisk matches the whole field range, a hyphen builds an inclusive range (`9-17`), a comma enumerates discrete values (`0,30`) and a slash applies a step to a range: `*/15` in the minute field fires at minutes 0, 15, 30 and 45. Steps can start from an offset, so `5/15` yields 5, 20, 35 and 50. The least intuitive rule in crontab(5) is that when the day-of-month and day-of-week fields are both restricted (neither one is an asterisk), cron ORs them instead of ANDing them. That makes `0 0 13 * 5` run on the 13th of every month and again on every Friday.
3. The six-field Quartz dialect and daylight saving time
Quartz, Spring and Jenkins use a six- or seven-field dialect that prepends seconds and optionally appends a year. That dialect adds `L` (last day), `W` (nearest weekday) and `#` for the nth weekday of the month, where `6#3` is the third Friday, and it requires a `?` in one of the two day fields. Daylight saving time breaks any local-time schedule: a job pinned to 02:30 never runs on the spring-forward night and runs twice on the fall-back night. Declaring `CRON_TZ=UTC` at the top of the crontab, or scheduling outside the 01:00-03:00 window, removes the ambiguity. AWS EventBridge always evaluates its expressions in UTC.
โ Frequently asked questions about cron expressions
Why does `0 0 13 * 5` not mean "Friday the 13th"?
Because cron ORs the day-of-month and day-of-week fields whenever both are restricted. That expression fires on the 13th of every month and again on every Friday. A real Friday-the-13th job has to be scheduled as `0 0 13 * *` and then check the weekday inside the script before doing any work.
How do I run a job every 15 minutes but offset by 5 minutes?
Use a step with a start value: `5/15 * * * *` fires at minutes 5, 20, 35 and 50, which is identical to listing `5,20,35,50`. Watch out for `*/7`: it produces 0, 7, 14, 21, 28, 35, 42, 49 and 56, then jumps to minute 0 of the next hour, leaving a 4-minute gap instead of 7.
Why does my script work in the terminal but fail under cron?
Because cron never sources your shell profile. The job starts with `SHELL=/bin/sh`, a minimal `PATH` (usually `/usr/bin:/bin`) and none of the variables exported by `.bashrc` or `.profile`. Call binaries by absolute path, set `PATH=` inside the crontab itself, and redirect output with `>> /var/log/job.log 2>&1`; otherwise cron mails it to the local spool and the errors go unnoticed.