How to read a cron expression
A cron expression is five values separated by spaces, read left to right as minute, hour, day of month, month, day of week. The job runs when the current time matches every field.
30 4 * * 1
│ │ │ │ │
│ │ │ │ └── day of week (1 = Monday)
│ │ │ └──── month (any)
│ │ └────── day of month (any)
│ └──────── hour (4am)
└─────────── minute (30)
That one reads "at 04:30 every Monday".
The five fields
| # | Field | Allowed values | Notes |
|---|---|---|---|
| 1 | Minute | 0 to 59 | |
| 2 | Hour | 0 to 23 | 24 hour clock, 0 is midnight |
| 3 | Day of month | 1 to 31 | |
| 4 | Month | 1 to 12, or JAN to DEC | |
| 5 | Day of week | 0 to 6, or SUN to SAT | 0 is Sunday, and most cron implementations also accept 7 |
Some schedulers use a different shape. Quartz, used by a lot of Java software, puts seconds first and adds an optional year, and numbers its weekdays 1 to 7 starting at Sunday. If an expression has six or seven fields, check which system it was written for before trusting your reading of it.
The operators
There are four, and they combine within a single field.
| Symbol | Name | Example | Meaning |
|---|---|---|---|
* | Any | * * * * * | Every minute of every day |
, | List | 0 9,13,17 * * * | At 09:00, 13:00 and 17:00 |
- | Range | 0 9-17 * * * | Hourly from 09:00 to 17:00 inclusive |
/ | Step | */15 * * * * | At :00, :15, :30 and :45 |
A step is always applied to a range. */15 in the minute field means "starting at 0, take every 15th value up to 59". You can step an explicit range too: 5-30/10 gives 5, 15 and 25, then stops because 35 is past the end.
Steps do not mean "every 15 minutes from now"; they select fixed values off the clock. */40 fires at :00 and :40, then waits only 20 minutes for the next hour's :00, because the count restarts each hour. Lists can contain ranges and steps, so 0 0-6/2,12,18-23 * * * is a valid hour field.
Names and shortcuts
The month and day of week fields accept three letter names, and case does not matter, so JAN, jan and Jan are the same. The classic crontab documentation says ranges and lists of names are not allowed, though many modern implementations do accept MON-FRI. If you are not certain what runs your job, use numbers: 1-5 is unambiguous everywhere.
Several shortcuts replace the whole five field expression:
| Shortcut | Equivalent | Runs |
|---|---|---|
@yearly or @annually | 0 0 1 1 * | Midnight, 1 January |
@monthly | 0 0 1 * * | Midnight, 1st of the month |
@weekly | 0 0 * * 0 | Midnight on Sunday |
@daily or @midnight | 0 0 * * * | Midnight daily |
@hourly | 0 * * * * | On the hour |
@reboot also exists, but it is not a schedule: it runs the job once when cron itself starts.
The day of month and day of week trap
This is the one that bites. When both the day of month field and the day of week field are restricted, meaning neither is *, cron runs the job when either matches, not when both do.
0 0 13 * 5
You might read that as "midnight on Friday the 13th". It actually means "midnight on the 13th of every month, and also midnight every Friday": more than sixty runs a year instead of one or two.
The AND behaviour you expected applies only when one of the two fields is *. So 0 0 * * 5 is every Friday and 0 0 13 * * is every 13th, both perfectly normal. It is constraining both that triggers the OR.
There is no standard way to express "Friday the 13th" in five fields. The usual workaround is to schedule every 13th and have the job check the weekday before doing any work.
A related subtlety: some implementations decide whether a field is restricted by checking whether it literally begins with *. Under those, 0-6 in the day of week field counts as restricted even though it covers every day, quietly switching on the OR behaviour. Use * when you mean "any".
Mistakes worth checking for
* */2 * * *runs every minute during every second hour: 720 runs a day. You wanted0 */2 * * *, which is 12.- A missing minute field shifts everything left by one and usually still parses, giving you a valid schedule at completely the wrong time.
0 0 31 * *silently skips the months that have no 31st.- Cron uses the system or user time zone unless the scheduler lets you set one. When clocks shift for daylight saving, a job in the skipped hour may not run at all and one in the repeated hour may run twice, depending on the implementation. Jobs that must run once a day are safest scheduled away from the small hours where the shift lands.
When in doubt, read the expression back as a sentence, then check the next few times it would actually fire. If those dates are not what you described, the expression is wrong, not your reading of it.