Guide
Reading a cron expression
Five numbers and some punctuation. Cron syntax is small enough to learn in ten minutes and strange enough that people who have used it for years still misread one particular case — the one where two fields are OR-ed together instead of AND-ed.
The five fields
* * * * *
│ │ │ │ │
│ │ │ │ └── day of week 0-7 (0 and 7 are both Sunday)
│ │ │ └───── month 1-12
│ │ └──────── day of month 1-31
│ └─────────── hour 0-23
└────────────── minute 0-59
A job runs when the current time matches every field. 30 4 * * * is 04:30 every day: minute 30, hour 4, any day, any month, any weekday. Read right to left if it helps — "any weekday, any month, any day, hour 4, minute 30".
Note what is missing: there is no seconds field in standard Unix cron, and no year. The finest granularity is one minute.
The four operators
*— every value the field allows.,— a list.0,15,30,45in the minute field is four specific minutes.-— an inclusive range.9-17in the hour field is nine values, 9 through 17./— a step, applied to whatever precedes it.*/15in the minute field is 0, 15, 30, 45.
Steps are where the mental model most often breaks. */n does not mean "every n minutes from now". It means "every value in this field that is divisible by n" — the field is a set of allowed values, and the step filters that set. The distinction is invisible for 15 and glaring for 90:
*/15 * * * * -> :00 :15 :30 :45 as expected
*/90 * * * * -> invalid; 90 is out of range for a 0-59 field
There is no way to express "every 90 minutes" in one expression, because the schedule does not repeat on an hour boundary. You would write two entries, or move the interval into the job itself.
Steps combine with ranges, which is the readable way to express a working-hours schedule:
0 9-17/2 * * 1-5 -> 09:00 11:00 13:00 15:00 17:00, Mon-Fri
A bare */n is shorthand for first-last/n, so */20 in the hour field means 0, 20 — and then stops, because 40 is out of range. It does not wrap.
The day-of-month and day-of-week rule
This is the one that surprises people. Every other pair of fields is AND-ed: the job runs when the minute matches and the hour matches and the month matches. Day-of-month and day-of-week are different.
If both are restricted — neither is * — they are OR-ed. The job runs when either matches.
0 0 13 * 5 -> midnight on the 13th of any month,
AND midnight every Friday.
Not "Friday the 13th".
To actually get Friday the 13th you need the job to run on every 13th and check the weekday itself, or use a cron implementation with extra syntax for it.
When one of the two is *, the rule collapses to the intuitive behaviour, which is why it goes unnoticed for so long:
0 0 * * 5 -> every Friday. Fine.
0 0 13 * * -> the 13th of every month. Fine.
0 0 1 * 1 -> the 1st of the month, and every Monday. Surprise.
This behaviour comes from Vixie cron and is what the crontab(5) man page specifies, so Linux, macOS, and BSD all do it. Quartz — used by Java and .NET schedulers — refuses the ambiguity instead: one of the two fields must be ?, meaning "no value specified".
Names, and the two Sundays
Months and weekdays accept three-letter English abbreviations, case-insensitive: JAN–DEC and SUN–SAT. 0 3 * * SUN is clearer than 0 3 * * 0 and identical.
The weekday field accepts 0 through 7, with both 0 and 7 meaning Sunday. That is not a typo in someone's crontab; it is deliberate, so that people who count weeks from Monday and people who count from Sunday can both write what they expect. It does mean 1-7 is not "all seven days" — it is Monday through Sunday, which happens to be all seven days, whereas 0-6 is Sunday through Saturday, also all seven. Both work; * is clearer.
Names cannot be used with steps or ranges in some older implementations, so MON-FRI is worth testing before relying on it in a constrained environment.
Shorthand strings
Most implementations accept a handful of named schedules in place of all five fields:
@yearly (or @annually) 0 0 1 1 *
@monthly 0 0 1 * *
@weekly 0 0 * * 0
@daily (or @midnight) 0 0 * * *
@hourly 0 * * * *
@reboot once, at daemon start
@reboot is the odd one out — it is not a time expression at all, and it fires when cron itself starts, which is usually but not necessarily at boot. Under systemd, a unit is the more predictable way to run something at startup.
Six-field variants
If an expression has six fields, the extra one is almost always seconds at the front — Quartz, Spring's @Scheduled, and several Go and Node libraries do this. Some Quartz dialects allow a seventh field for the year at the end.
0 */5 * * * * Spring: every 5 minutes, at second 0
*/5 * * * * Unix: every 5 minutes
The two are not distinguishable by eye without counting fields, and pasting a five-field expression into a six-field parser shifts everything one position left — your minute becomes seconds, your hour becomes minutes, and the job runs far more often than intended. Count the fields before trusting a copied expression.
Time zones and daylight saving
A cron expression has no time zone. It is interpreted in whatever zone the daemon is running in, which on a server is often UTC and on a laptop is local time. Moving a crontab between machines can silently shift every schedule.
Where local time is used and that zone observes daylight saving, two things happen twice a year:
- Spring forward. The clock jumps from 02:00 to 03:00. A job scheduled at 02:30 has no 02:30 to run at. Vixie cron compensates for jobs scheduled in the skipped hour by running them once when the clock jumps; a naive scheduler skips the day entirely.
- Fall back. 02:00–03:00 happens twice. A job at 02:30 could run twice. Vixie cron suppresses the second occurrence for fixed-time jobs — but wildcard jobs like
*/10 * * * *are treated as interval jobs and do run through both passes.
The practical rules that avoid all of it: run the daemon in UTC, and if a job must not run twice, make it idempotent or take a lock. Anything scheduled between 01:00 and 03:00 local time in a DST zone is asking for one of these two behaviours.
Mistakes worth checking for
* * * * *— every minute. Usually written by accident when someone meant a single specific time.0 0 * * 0versus0 0 * * *— one is weekly on Sunday, the other is daily. A single character apart.- Both day fields set. Re-read the OR rule above.
- A five-field expression in a six-field scheduler, or the reverse.
%in the command. In crontab, an unescaped%is a newline and everything after the first one becomes stdin for the job. Dates likedate +%Y-%m-%dmust be writtendate +\%Y-\%m-\%d.- Assuming an environment. Cron runs with a minimal
PATHand none of your shell profile. Use absolute paths, or the job works interactively and fails on schedule.
Last updated 10 August 2026