Cron on the first Monday of the month: the OR rule trap
The obvious expression, 0 9 1-7 * 1, is wrong. When both the day of month and the day of week fields are restricted, cron fires when either matches, so it runs about eleven days a month instead of one.
At a glance
- Expression
- 0 9 1-7 * * with a weekday guard
- Fires
- Once a month if done correctly. The naive expression fires ten or eleven times a month.
- Restricted fields
- Day of month restricted to 1-7 in the working version, with the weekday test moved into the script.
- Equivalent macro
- None.
- What the naive expression really does
- Fires on days 1 to 7 and on every Monday, which is ten or eleven days in a typical month.
- Why the first Monday is always in days 1 to 7
- Any seven consecutive days contain exactly one of each weekday.
In detail
The reasoning behind 0 9 1-7 * 1 is sound: the first Monday of a month must fall somewhere in the first seven days, so restrict the date to that window and the weekday to Monday. The problem is that cron does not combine those two fields with AND. When both the day of month and the day of week fields are restricted, a date matches if either one matches.
So that expression fires on the 1st through the 7th whatever weekday they are, and on every Monday whatever date it is. In a month beginning on a Wednesday that is seven days plus three more Mondays, ten runs. In a month beginning on a Monday it is seven days plus four more Mondays, eleven. Either way it is not the first Monday.
The fix is to restrict only one of the two fields in the expression and do the other test in the script. Keep the day of month range, which narrows the schedule to seven runs a month, and have the script check the weekday and exit unless it is Monday. Six of the seven runs cost nothing.
The same shape covers any nth weekday of the month. The second Tuesday lives in days 8 to 14, the third Thursday in days 15 to 21, and the fourth Friday in days 22 to 28. Pick the seven day window, put it in the day of month field, leave the day of week field as an asterisk, and test the weekday in the script.
Examples
0 9 1-7 * * [ "$(date +\%u)" = "1" ] && /opt/app/bin/first-monday.shdate +%u gives 1 for Monday through 7 for Sunday. The percent is escaped for cron.
0 9 1-7 * 1Fires on days 1 to 7 and on every Monday. Ten or eleven runs a month.
0 9 8-14 * * [ "$(date +\%u)" = "2" ] && /opt/app/bin/second-tuesday.shThings that catch people out
- The naive expression parses cleanly and runs. Nothing warns you, and the extra runs look like the job being triggered by something else.
- date +%u numbers Monday as 1 and Sunday as 7, while the cron day of week field numbers Sunday as 0. The two conventions do not match.
- Leaving the day of week field as an asterisk is what keeps the OR rule out of the picture. Restricting it, even to the correct day, is what causes the problem.
Try it: Cron Expression Parser
Paste any five or six field expression to get a plain English description, a per field breakdown and the next run times in your local time.
Cron Expression Parser
LOCAL · Browser-OnlyParse cron expressions and see the next scheduled run times
Supports 5-field (minute hour day month weekday), 6-field (with seconds), named values (JAN, MON), and @macros (@daily, @hourly, ...)
Use the dropdowns below to build your cron expression visually, or type a custom value in each field.
Human-Readable Description
Every 5 minutes
Field Breakdown
| Field | Allowed | Value | Meaning |
|---|---|---|---|
| Minute | 0-59 | */5 | every 5 minute(s) |
| Hour | 0-23 | * | every hour |
| Day of Month | 1-31 | * | every day of month |
| Month | 1-12 | * | every month |
| Day of Week | 0-7 | * | every day of week |
Related cron expressions
All cron expressions →Cron day of month and day of week: the OR rule
When both date fields are restricted, cron fires if either matches, not both. This turns narrowing expressions into much wider ones without any error.
Cron on the last Friday of the month: no # or L support
The Quartz forms 6L and 6#5 are rejected here. Schedule every Friday and let the script check whether the following Friday is in the next month.
Cron on the last day of the month: the L workaround
This parser rejects the Quartz L character, so the last day of the month needs a date range plus a script guard that checks whether tomorrow is the 1st.
The cron day of week field: 0 to 7 with Sunday at both ends
The day of week field runs 0 to 7 with Sunday as both 0 and 7, and accepts SUN through SAT. Restricting it changes how the day of month field behaves.
Cron every 2 months: 0 0 1 */2 * and the odd month set
0 0 1 */2 * fires on the 1st of January, March, May, July, September and November. Use 0 0 1 2/2 * for the even months instead.
Cron every quarter: 0 0 1 */3 *
0 0 1 */3 * runs on the 1st of January, April, July and October, four times a year. Shift it with 2/3 for a fiscal year starting in February.
Tools for this
- Cron Expression Parser Free online cron expression parser - parse cron expressions and see the next scheduled run times
- Crontab Generator Free online crontab generator - build cron expressions visually with an intuitive interface