Writing cron expressions that do not fire surprises
The five fields and their ranges, names versus numbers, the day-of-month and day-of-week OR trap, Sunday as 0 or 7, daylight-saving gaps and overlaps, and validating with a next-runs preview before deploying.
Five fields, read left to right
A classic cron expression is five space-separated fields: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–7). Each field accepts a single value, a comma-separated list, a hyphenated range, a step after a slash, or an asterisk for every value.
Most cron bugs are not exotic syntax but an ordinary field read carelessly: a step that resets at the range boundary, or an hour field written for a twelve-hour clock in a system that counts to twenty-three.
- ListsLists pick discrete values: 0,30 in the minute field fires at the top of the hour and at half past.
- Ranges and stepsRanges pick a span: 9-17 in the hour field covers business hours, and a step thins it out—9-17/2 fires every second hour inside the span.
- The asterisk matches allThe asterisk is not “ignore this field”; it actively matches every value, which becomes important the moment two day fields interact.
Names are numbers with better documentation
Month and weekday fields accept three-letter English names—JAN through DEC and SUN through SAT—as aliases for their numbers. 0 9 * JAN MON and 0 9 * 1 1 describe the same schedule, but the named version explains itself to the next reader and resists off-by-one edits.
- Case-insensitiveNames parse case-insensitively—mon, Mon, and MON all work—but upper case is the convention that reads as intentional.
- Names in rangesNames work inside ranges and lists too: MON-FRI is the standard weekday span, and JAN,APR,JUL,OCT picks the quarterly months.
- Read the explanationCron Builder & Explainer normalizes the expression and explains each field separately—“month: every value”, “weekday: 1, 2, 3, 4, 5”—so you can confirm the parser read the names the way you meant them.
How the two day fields interact
This tool follows Vixie-style day rules: when neither day field starts with *, a match in either field is enough. 0 9 13 * FRI does not mean “Friday the 13th”; it means the 13th of every month and every Friday.
The explainer’s output is explicit about this: it reports the semantics as dom-dow-or alongside the computed runs, so an expression that will fire far more often than intended shows its hand before it reaches a crontab.
- Wildcard steps still constrainIf a day field starts with *, including a step such as */2, this tool requires both day fields to match; the step restriction still applies.
- No leading wildcard means ORWhen neither day field starts with *, this tool applies OR: the day of month or the weekday may match.
- AND needs a workaroundTo hit a genuine Friday-the-13th, let cron select the 13th and have the command itself check the weekday—or use a scheduler with AND semantics, which classic cron is not.
Sunday is 0, and usually also 7
The weekday field numbers Sunday as 0, Monday as 1, and so on through Saturday as 6. Most implementations also accept 7 as a second Sunday for people counting from Monday—the Toolars parser normalizes 7 to 0 internally—but some systems accept only one of the two, and ISO-style numbering starting Monday at 1 exists elsewhere.
- 0 and 70 and 7 are both Sunday where 7 is accepted; writing 0 is the portable choice across schedulers.
- Silent off-by-oneA weekday of 7 on a system that rejects it fails loudly at deploy time, but a weekday misread as “seventh day counting from Monday” fails silently on the wrong day.
- Re-confirm per platformWhen an expression moves between implementations—system crontab, a container platform, a CI scheduler—re-confirm the weekday numbering instead of assuming it survived the trip.
Daylight saving bends the schedule
Daylight-saving transitions create missing and repeated local minutes. This preview skips a nonexistent time such as 02:30 in a spring-forward gap and lists a repeated matching minute twice in an autumn overlap. A deployed scheduler may catch up or suppress a duplicate; verify its own policy.
- The missing hourSpring gap: the preview has no matching 02:30 minute; some schedulers run missed jobs after the transition.
- The doubled hourAutumn overlap: the preview can list both matching instants, while an actual scheduler may suppress the second run.
- UTC has no DSTThe clean escape is UTC: it has no daylight-saving transitions, so 30 2 * * * means the same instant every day of the year. The explainer offers UTC, the device’s local zone, and named zones such as America/New_York and Europe/London.
Validate with the next runs, not with hope
The difference between a correct expression and a plausible-looking one is a list of concrete fire times. Cron Builder & Explainer computes the next ten runs from now in the time zone you choose, searching up to 366 days ahead, and evaluates in a background worker so a heavy expression cannot stall the tab.
- Read the runsRead the first few runs against intent: “weekdays at 09:00” should show Monday through Friday at nine, and a Saturday in the list is the OR trap announcing itself.
- Cross a transitionSelect a named time zone and inspect upcoming runs if the next daylight-saving transition falls inside the 366-day preview. The workspace starts from now; it cannot choose a custom start date.
- Explicit errorsInvalid expressions report errors. Zero matches within 366 days report a search-limit error; one to nine matches return a partial result rather than an empty list.
Frequently asked questions
Why did my cron job fire on days I did not expect?
Is Sunday 0 or 7 in cron?
How do I make a schedule immune to daylight saving time?
Schedules and patterns share a discipline.
Both are tiny strings with an outsized blast radius—learn the write-test-refactor loop for regular expressions, run in a local worker with a timeout.