A teammate pasted this cron: 0 9 star star 1-5, saying it runs Monday to Friday at 9 AM. I looked at it and told him it would not fire this Monday. Not a joke — every one of the 5 fields has its own rules. Get any of them wrong, and your schedule is not what you think it is.
This guide is so that next time you read a cron expression, you do not need to open the crontab(5) man page to figure out what it actually does.
30-second overview
- A cron expression has 5 fields, left to right: minute, hour, day-of-month, month, day-of-week.
- Each field can be star (any), a number, a range (9-17), a step (star/15), or a list (1,15).
- Sunday in the day-of-week field is 0 (and 7 is accepted as an alias). Never write 7 for portability — some schedulers reject it.
- When both day-of-month and day-of-week are restricted (neither is star), the firing rule is OR: any match on either counts.
- On a DST transition day, cron may miss a run or fire twice. That is a cron limitation, not a bug.
5 real scenarios: field-by-field pitfalls
Scenario 1: star/15 in minute — you do not actually know what it does
A teammate wrote star/15 star star star star thinking every 15 minutes. Reality: minute=0, 15, 30, 45.
Trap: many programmers assume star/15 starts counting from the current minute, then every 15 after that. Wrong. It starts from the field lower bound (0) and selects values by step. So a schedule starting at 5:07:23 first runs at 5:15:00, not 5:22:23.
Fix: use explicit values, or open star/15 in a cron generator and check the preview. star/5 means 0, 5, 10, …, 55 — five values, five-minute intervals.
Scenario 2: hour range 9-17
Old tutorials say work hours are 9-17. The expression 0 9-17 star star star means it fires at hour=9, 10, 11, 12, 13, 14, 15, 16, 17 — nine different hours, once each.
Trap: it is not 9 AM through 5 PM as a window. It does not fire at 17:30 (the 17:00:00 run is the last), and nothing fires after 18:00. For every-30-minutes during work hours, write star/30 9-17 star star star — 9:00, 9:30, 10:00, 10:30, …, 17:00, 17:30 — 18 runs per day across 9 hours.
Scenario 3: day-of-month star vs specific numbers
Task: run a report on the 1st of every month. 0 0 1 star star looks right, and there is no issue here.
Trap: teams sometimes want last-day-of-month instead and write 0 0 L star star. But L is a Quartz extension; Vixie cron (the default Linux cron) does not recognize it. For Quartz support, you need either a Quartz scheduler or to handle it in the script.
Fix: a reliable end-of-month approach is weekday fallback. If the last day is a weekend, run on the preceding Friday. Use 0 0 star star 1-5 with day-of-month range 28-31, and let the script decide whether today is a working last-day.
Scenario 4: Sunday in day-of-week is 0, not 7
Task: back up every Sunday at 3 AM. Teammate writes 0 3 star star SUN — you know that is Sunday. But if he writes 0 3 star star 7, what should you interpret?
Trap: Vixie cron / cronie accept both 0 and 7 as Sunday. Some schedulers (early Quartz, some early AWS EventBridge configs) only accept 0 and reject 7.
Fix: always write 0. It is the RFC standard and has the best portability. Treat 7 as a parser-specific extension.
Scenario 5: month shorthand JAN FEB MAR … DEC
Documentation example: 0 9 star JAN-MAR star. Valid syntax, equivalent to 0 9 star 1-3 star.
Trap: shorthand only works in the day-of-week field (SUN MON TUE WED THU FRI SAT) and the month field (JAN … DEC). You cannot use shorthand in minute / hour / day-of-month.
Fix: if your team is comfortable reading shorthand, use it; when in doubt, fall back to 0-6 for weekday and 1-12 for month.
4 counterintuitive facts about cron
Fact 1: day-of-month and day-of-week are OR, not AND
Task: fire on Sunday every week, OR on the 1st of every month. 0 0 1 star 0 reads as: only when both 1st-of-month and Sunday coincide. Wrong.
Actual rule: when both day-of-month and day-of-week are restricted (neither is star), the OR rule applies. Any match on either fires. So 0 0 1 star 0 = 1st-of-month runs + every-Sunday runs (which can produce 1-2 fires in a given month depending on calendar).
For strict AND semantics, use a script or switch schedulers (k8s CronJob supports richer field semantics).
Fact 2: star means different things in different fields
star in minute = any 0-59, every minute. star in hour = any 0-23, every hour on the hour. The difference is frequency — minute star is per-minute, other fields star means the maximum allowed frequency for that field.
Fact 3: star/S is equivalent to 0/S
star/15 is the same as 0/15, in the minute field where the lower bound is 0. 9-17/2 yields 9, 11, 13, 15, 17 — five values.
Trap: writing 1/15 in the minute field gives 1, 16, 31, 46 — four values, not every-15-minutes starting from 1.
Fact 4: DST days may miss a run or fire twice
On North American DST transitions (second Sunday of March / first Sunday of November), local time skips or repeats an hour. Cron schedules against local wall-clock time, and implementations vary.
Fix: for time-sensitive tasks, do not use plain cron. Use systemd timer or k8s CronJob in UTC with explicit timezone — no DST drift.
3 real schedule templates
Template 1: every 15 minutes during work hours
star/15 9-17 star star 1-5
Meaning: Monday through Friday, 9 AM through 5 PM inclusive, every 15 minutes.
Frequency: 9 hours × 4 per hour = 36 runs per workday, no weekends. Want fewer? star/30 gives 18 per day.
Template 2: end-of-week Friday at 6 PM
0 18 star star 5
Note: day-of-week value is 5, not FRI — they are equivalent. Test which your deployment’s cron accepts; some implementations reject the shorthand.
Frequency: once per week, Friday 6 PM. By Monday morning the report should already be in the inbox.
Template 3: monthly 1st midnight backup, fallback to preceding Friday if 1st is a weekend
This cannot be expressed in plain cron — needs a wrapper script.
Reference implementation: a bash script (a.sh) that first checks via date if today is the first. If yes, run the backup. crontab then runs 0 0 28-31 star star 1-5 /path/to/a.sh — last-week workday of each month at midnight, script checks whether today is the day-before-1st, and runs the backup if so.
Recommended practices
- Do not write cron blind — open crontab.guru or our cron generator and visualize the fields, check the human-readable description.
- Always check next-run list before deploying — piick cron-generator defaults to next 5 runs; paste your expression, see whether the times match your expected window.
- For time-sensitive tasks use UTC plus explicit TZ — DST is cron’s original sin; systemd timer or k8s CronJob with spec.timezone=UTC sidesteps it.
- Do not stuff last-day-of-month into day-of-month 31 — L is Quartz and unreliable cross-platform; let the script decide.
- Critical tasks need timeout and retry — cron does not care whether a task finished; kill -9 timeout still leaves the task running. Use systemd OnFailure for retries.
Cron is one of Unix’s most practical tools — 5 fields, simple syntax, cross-platform. We built cron-generator to turn fields into dropdowns plus real-time preview plus next-run list, so it is harder to fall into a pit when writing them.
Try our cron generator tool, switch to Visual Builder or Raw Editor, paste star/15 9-17 star star 1-5, check whether next 5 runs fall in weekday 9-17 work hours. If yes — congratulations, your first cron schedule lands. If not — use the next-runs feedback to find which field is wrong.