Cron Syntax Explained: A Cheatsheet of the Schedules Everyone Actually Uses
· by Andergrove Software
Cron expressions are five fields of pure convention — no types, no validation, no error messages until 3 a.m. when the job didn’t run. Nearly everyone who writes one starts from a half-remembered example and mutates it until it looks right. This is the reference for that moment: how the five fields actually work, a cheatsheet of the schedules that cover 95% of real jobs, and the two behaviours — the day-of-month/day-of-week rule and time zones — that cause almost every cron surprise in production.
The five fields, left to right
A standard cron expression is five space-separated fields, in this order:
┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12 or JAN–DEC)
│ │ │ │ ┌─ day of week (0–7, 0 and 7 = Sunday, or SUN–SAT)
│ │ │ │ │
* * * * *
Each field takes a value, a list (1,15), a range (9-17), a step (*/5), or * meaning “every”. That is the whole language. The complexity comes from combining them: 0 9-17/2 * * MON-FRI reads “at minute 0, every 2nd hour from 9 to 17, on weekdays” — every part is one of those five constructs.
Two common extensions to know. Some systems (Quartz, Spring) add a seconds field at the front, making six or seven fields — if a cron expression has six fields, the first one is seconds, not minutes. And most crons accept shorthand macros: @hourly, @daily, @weekly, @monthly, @reboot. They are readable, but the five-field forms are portable everywhere.
The cheatsheet
These cover almost every schedule real systems run. Copy, adjust the numbers, done.
| Expression | Meaning |
|---|---|
*/5 * * * * | Every 5 minutes |
*/15 * * * * | Every 15 minutes |
0 * * * * | Every hour, on the hour |
0 */6 * * * | Every 6 hours (00:00, 06:00, 12:00, 18:00) |
0 0 * * * | Daily at midnight |
30 2 * * * | Daily at 02:30 (a classic backup slot) |
0 9 * * 1-5 | Weekdays at 09:00 |
0 0 * * 0 | Weekly, Sunday at midnight |
0 0 1 * * | Monthly, 1st at midnight |
0 0 1 1 * | Yearly, January 1 at midnight |
0 8,12,17 * * * | Three fixed times a day (08:00, 12:00, 17:00) |
*/10 9-17 * * 1-5 | Every 10 minutes during business hours, weekdays |
Note what “every 5 minutes” actually means: */5 in the minute field fires at minutes 0, 5, 10… of every hour — steps count from the start of the field’s range, not from when the job was installed. */7 therefore fires at 0, 7, 14, 21, 28, 35, 42, 49, 56 and then again at 0 — an uneven 4-minute gap at the hour boundary. Cron cannot express “exactly every 7 minutes”; if you need true fixed intervals that don’t divide 60, use a systemd timer or your scheduler’s rate syntax instead.
The trap: day-of-month OR day-of-week
Here is the rule that has surprised sysadmins for forty years. When both the day-of-month and day-of-week fields are restricted (neither is *), standard cron runs the job when either matches — not both.
0 0 13 * 5 # midnight on every Friday AND every 13th — not just Friday the 13th
That expression fires roughly six times a month, not once or twice a year. The behaviour is specified by POSIX, so it is not a bug you can patch around — it is what the expression means. If you genuinely need “Friday the 13th”, the standard trick is to schedule for the 13th and test the weekday in the command itself: 0 0 13 * * [ "$(date +\%u)" = 5 ] && /path/to/job. (And note the escaping: inside a real crontab, percent signs must be written \% — an unescaped % is treated as a newline, which is its own classic debugging story.)
When only one of the two fields is restricted, everything behaves intuitively: 0 9 * * 1-5 is simply weekdays at nine. The OR rule only arms itself when you constrain both.
Time zones, DST, and the 2:30 job that didn’t run
Cron fields describe wall-clock time in whatever time zone the cron daemon uses — usually the system time zone, which on a server is hopefully UTC but often isn’t. Two consequences:
First, the DST hole. In a zone that observes daylight saving, the spring-forward night skips an hour: in most of the US, 2:00–2:59 a.m. simply does not occur one Sunday in March. A job scheduled at 30 2 * * * silently doesn’t run that night (modern cron implementations try to compensate; many older ones don’t). The autumn transition has the mirror problem — the hour happens twice. The boring, correct fix: run servers in UTC and schedule in UTC, converting to local time only for display.
Second, distributed confusion. Kubernetes CronJobs, GitHub Actions schedules, and most cloud schedulers default to UTC regardless of where you sit — so 0 9 * * * in a GitHub Actions workflow is 9:00 UTC, which is 7 or 8 p.m. in Sydney depending on the season. When a scheduled job seems to fire at a bizarre time, the time zone mismatch is the first suspect, and our time zone converter resolves it in seconds.
Reading an unfamiliar expression
Writing cron is easier than reading it — 0 4 8-14 * * takes most people a minute to decode (04:00 on the 8th through 14th of each month, i.e. the second week). The reliable method is to read the fields right to left: constrain the weekday first, then month, then day, then time of day. Anything with steps or combined lists deserves a second opinion.
That is what our cron expression parser is for: paste any five-field expression (or a macro like @daily) and it renders the schedule in plain English plus the next ten actual run times, in any time zone — which catches both the OR-rule surprise and time zone mix-ups before they reach production. It runs entirely in your browser, and the companion guide covers the syntax in more depth.
One final habit worth stealing: put the plain-English schedule in a comment above every crontab line. The expression is for the machine; the comment is for whoever gets paged.