Cron Expression Parser
What a cron expression actually does, said in English, plus the next times it fires
Next runs
Nothing yet.
Field by field
Nothing yet.
Five numbers, each one a set of values, and a job that runs whenever the clock matches all of them at once.
+------------- minute 0-59
| +----------- hour 0-23
| | +--------- day of month 1-31
| | | +------- month 1-12 or JAN-DEC
| | | | +----- day of week 0-7 or SUN-SAT (0 and 7 are both Sunday)
| | | | |
* * * * *Each field takes a value, a list, a range, a step, or * for all of them. */5 is every fifth value from the bottom of the range,1-5 is a range, 1,15 is a list, and they combine:0-30/10,45 is 0, 10, 20, 30 and 45.
The one rule that is an OR
0 0 1 * MON
Reads as: "midnight on the 1st, and midnight every Monday"
Not as: "midnight on the 1st, if it is a Monday"
Roughly 5 runs a month, not 1 or 2 a year.Day-of-month and day-of-week are both about which day, and when both are restricted the job runs when either matches. Every other combination of fields is an AND. This is not a quirk of one implementation; it is in the POSIX specification and in Vixie cron's man page, and it has been catching people out for forty years.
When both fields are set here, that is called out above the results. If you want "the first Monday of the month", five fields cannot say it. The usual workaround is 0 0 1-7 * MON with a check inside the job itself, or Quartz's MON#1 if you are on Quartz.
Steps do not span the field
*/20 * * * * :00 :20 :40 evenly spaced
0,20,40 * * * * the same thing, written out
*/45 * * * * :00 :45 then back to :00
a 45-minute gap and then a 15-minute one,
because the pattern restarts every hourA step counts from the bottom of the field and stops at the top. It does not carry over. So */45 in the minute field does not mean "every 45 minutes"; it means "at minute 0 and minute 45 of every hour", which is a 45-minute gap followed by a 15-minute one, forever.
Anything that does not divide evenly into 60 has this problem: 7, 8, 9, 11, 13, 14, 25, 40, 45. Easiest way to spot it is the next-run list above. The gaps stop being equal.
Four dialects, and the day they disagree about
Unix min hour dom mon dow Sunday is 0 and 7
Seconds sec min hour dom mon dow Sunday is 0 and 7
Quartz sec min hour dom mon dow [year] Sunday is 1
EventBridge min hour dom mon dow year Sunday is 1
The same five characters can mean different days in different
schedulers. 0 0 * * 1 is Monday everywhere except Quartz and
EventBridge, where it is Sunday.Quartz adds three modifiers that plain cron has no way to express, and EventBridge borrowed them:
| Written | Means |
|---|---|
L in day-of-month | The last day of the month, whatever its number |
LW | The last weekday of the month |
15W | The weekday nearest the 15th, without crossing into another month |
FRI#3 | The third Friday of the month |
FRIL | The last Friday of the month |
? | No value here. Required in one of the two day fields |
? exists precisely because of the OR rule above. By insisting that one of the two day fields carries no value, Quartz and EventBridge make the ambiguity impossible to write.
Cron does not know about time zones, until it does
A crontab entry runs against the system clock of the machine it is on. Move the machine, change the server's zone, or run the same job in two regions, and the schedule moves with it.
Then twice a year the clock itself moves:
- Forward. One hour never happens. A job at 01:30 simply does not run that day on most implementations. Vixie cron makes an exception and runs jobs scheduled in the skipped hour immediately afterwards; systemd timers and most cloud schedulers do not.
- Back. One hour happens twice. Vixie cron will not repeat a job with a fixed time, but wildcard schedules such as
*/10 * * * *genuinely run twice.
Runs that fall in a skipped hour are left out of the list above and counted in a note, because pretending they exist would be worse than saying they do not. Dodge the whole problem by scheduling in UTC, as Kubernetes CronJobs, EventBridge and most container schedulers do by default.
Where cron is not what you want
Cron answers "when", never "did it work". It has no notion of a job that is still running when the next one is due, no retry, no backoff, no history, and no alert when a run fails. A schedule that overlaps itself will happily start a second copy on top of the first.
Two habits cover most of it: take a lock at the start of the job so a slow run cannot be overtaken, and have the job report somewhere when it finishes, so that silence is detectable. Everything past that is what a scheduler with a queue is for.
Shorthands
| Shorthand | Same as |
|---|---|
@yearly, @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily, @midnight | 0 0 * * * |
@hourly | 0 * * * * |
@reboot | Once, when the machine starts. Not a schedule at all |
All of them run at the very top of their period, which means every@daily job on a machine starts at the same instant. Spreading them out by a few minutes each is worth doing before it becomes the reason something falls over at midnight.