How It WorksPublished 10 min read

cron’s Day-of-Month and Day-of-Week Are ORed: Writing Second Mondays, Month-End, Last Fridays and Every Other Week Correctly

You wrote “0 9 1-7 * 1” for “9:00 on the first Monday of every month”, and it ran every Monday plus every day from the 1st to the 7th. This very common cron mistake comes from a rule unique to cron: if you specify both the day of month and the day of week, the job runs on days that match either one. This article explains how the OR works and its exception, how to make crontab handle nth weekdays, month-end, last Fridays and every other week correctly, and how to use L, # and W in AWS EventBridge and Spring, with real run dates as examples.

Specify both day of month and weekday and it runs on either

Of a cron expression’s five fields, minute, hour and month are read the intuitive way: the job runs at times that satisfy all of them. The day of month (3rd field) and day of week (5th field) are special, though. If both contain something other than *, the job runs on days that satisfy either the day-of-month condition or the weekday condition. This has long been how Linux cron works (Vixie cron and descendants such as cronie), and GitHub Actions and Kubernetes CronJobs read it the same way.

For example, “0 9 1-7 * 1”, written to mean “9:00 on the first Monday of every month”, actually runs at 9:00 on “the 1st to 7th of every month” or “every Monday”. Counting from Monday, October 12, 2026, the actual run dates are:

Run dateCondition matched
Mon, Oct 12Weekday is Monday
Mon, Oct 19Weekday is Monday
Mon, Oct 26Weekday is Monday
Sun, Nov 1Day is 1–7
Mon, Nov 2Day is 1–7, and weekday is Monday
Tue, Nov 3Day is 1–7

Instead of only the first Monday, it runs every Monday and on the first seven days of each month, around 10 times a month. Set up a regular-meeting reminder or a monthly report this way and you’ll be sending notifications almost every week.

The exception: if either starts with *, it becomes AND

To make it more confusing, Linux cron has an exception: if the day-of-month or weekday value starts with *, it becomes AND (days satisfying both) instead of OR. Step values that start with *, such as “*/2”, fall into this case.

ExpressionHow it’s readDays it runs
0 10 1-31/2 * 2OR (either)Every odd-numbered day plus every Tuesday
0 10 */2 * 2AND (both)Only Tuesdays that fall on odd-numbered days

“1-31/2” and “*/2” both mean the 1st, 3rd, 5th…31st, yet that one difference in writing completely changes the run dates. Listing the runs of “0 10 */2 * 2” from October 2026 gives October 13, October 27, November 3, November 17, December 1…, only Tuesdays on odd-numbered days. This exception is a fine detail of the implementation, so the reading may change if you move to another scheduler. Rather than relying on it on purpose, it’s safer to remember it as “a form to avoid”.

How to run on the nth weekday of the month in crontab

crontab has no way to express “the second Tuesday” directly. Instead, you use the fact that the nth weekday always falls within the seven days from day (n−1)×7+1 to day n×7: days 1–7 for the first, 8–14 for the second, 15–21 for the third and 22–28 for the fourth.

  1. 1

    Narrow the day-of-month field to the seven days

    For 10:00 on the second Tuesday, set the day of month to 8-14 and the weekday to *: “0 10 8-14 * *”. Don’t put 2 in the weekday field, or it becomes an OR.

  2. 2

    Check the weekday at the start of the command

    date +%u returns the weekday as a number, with Monday as 1 and Sunday as 7. In crontab, escape % and write “[ "$(date +\%u)" = 2 ] && /path/to/command”, and the command after it only runs on Tuesdays.

  3. 3

    Confirm against the run dates

    As far as cron is concerned, it runs every day from the 8th to the 14th, so check through the script’s logs or a manual test that the command really only does its work on the second Tuesday.

In AWS EventBridge and Spring’s @Scheduled, you can write it directly with #. EventBridge numbers weekdays from 1 (Sunday), so 10:00 on the second Tuesday is “cron(0 10 ? * 3#2 *)”; in Spring it’s “0 0 10 ? * TUE#2”. Listing the EventBridge version from October 2026 gives October 13, November 10, December 8 and January 12, only the second Tuesday of each month.

The last day of the month and the last Friday (and the trap of specifying the 31st)

Writing “0 23 31 * *” for “month-end closing on the last day of every month” is a classic mistake. It never runs in February, April, June, September or November, which have no 31st. Likewise, the 30th never runs in February, and a date that doesn’t exist, such as “0 9 30 2 *” (February 30), gives an expression that never runs at all.

What you wantcrontabEventBridgeSpring
Last day of every month, 23:000 23 28-31 * * + last-day checkcron(0 23 L * ? *)0 0 23 L * *
3 days before month-end0 9 25-28 * * + date checkcron(0 9 L-3 * ? *)0 0 9 L-3 * *
Last Friday, 18:000 18 22-31 * * + weekday and week checkcron(0 18 ? * 6L *)0 0 18 ? * 5L
Last weekday (business day)0 18 26-31 * * + weekday checkcron(0 18 LW * ? *)0 0 18 LW * *
Weekday nearest the 15thNot possible (decide in the command)cron(0 9 15W * ? *)0 0 9 15W * *

To detect the last day in crontab, use the idea that “if tomorrow is the 1st, today is the last day”. With GNU date (standard on Linux), “0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/command” runs the command only on whichever of the 28th–31st is the last day. For the last Friday, narrow the day to 22-31 and then check that today is Friday and that the date 7 days from now is in next month (e.g. [ "$(date +\%u)" = 5 ] && [ "$(date -d '+7 days' +\%m)" != "$(date +\%m)" ] && command).

In EventBridge, L means “the last day of the month”, 6L means “the last Friday (6 is Friday)”, and 15W means “the weekday nearest the 15th”. Working out 15W for November 2026, for example: the 15th is a Sunday, so it runs on Monday the 16th. W never crosses into another month, so if the 1st is a Saturday, 1W runs on Monday the 3rd.

Every other week can’t be written in cron alone

“Every other Monday” can’t be expressed with cron’s five fields. “*/14” in the day-of-month field just means the 1st, 15th and 29th, and counting restarts from the 1st each month, so it isn’t every two weeks. There’s no week-number field either, so the standard approach is to decide in the command.

  • Run every Monday and only do the work in weeks with an even ISO week number: 0 9 * * 1 [ $(( $(date +\%-V) \% 2 )) -eq 0 ] && command (%-V drops the leading zero, as in 08; with the zero, 08 and 09 are read as octal in shell arithmetic and cause an error)
  • Week numbers go back to 1 after 52 or 53 at the turn of the year, so in a year with 53 weeks you can get two weeks in a row, or a three-week gap, around New Year
  • For a strict 14-day cycle, count the days elapsed since a reference date and run only on days divisible by 14 (e.g. divide the difference from the reference date’s UNIX time by 86400)
  • Recording the last run date in a file and running when 13 or more days have passed is also reliable

How the differences add up across schedulers, and how to check

How the day of month and weekday are treated depends on the system that reads the cron expression. The same expression can change meaning when you move it, so first confirm which system will run it.

SystemWhen both day of month and weekday are givenL, #, W
crontab (Linux)Either (OR); both (AND) if one starts with *Not available
GitHub ActionsSame as crontabNot available
Kubernetes CronJobSame as crontabNot available
AWS EventBridgeCan’t give both; set one to ?Available
Spring @ScheduledBoth (AND)Available
systemd timerBoth (AND)Has ~ to count back from the month’s end

In Spring, “0 0 9 1-7 * MON” means exactly “Mondays among the 1st to 7th of each month”, in other words the first Monday. Written with crontab habits, it means the opposite, so take particular care when porting. EventBridge syntax is explained in detail in how to write EventBridge cron expressions.

When you specify both the day of month and the weekday, the cron expression checker warns that the job “runs on days matching either one”, and it also tells you when a leading * makes it AND. It catches mistakes such as specifying only the 31st so some months never run, or a date that doesn’t exist so it never runs at all, and lists the upcoming run dates for you to check. For basic crontab syntax see how to write crontab, with a cheat sheet, and to run GitHub Actions on Japan time see running GitHub Actions cron on Japan time.

FAQ

How do I run a cron job on the second Monday of every month?
In crontab, set the day of month to 8-14 and the weekday to *, as in “0 9 8-14 * *”, and check for Monday at the start of the command ([ "$(date +\%u)" = 1 ] && command). If you put 1 in the weekday field it becomes an OR and also runs every Monday. In EventBridge you can write it directly as cron(0 9 ? * 2#2 *), and in Spring as 0 0 9 ? * MON#2.
How do I run cron on the last day of every month?
crontab has no way to express the last day, so set the day of month to 28-31 so it runs daily, and add a check to the command that runs it only if tomorrow is the 1st ([ "$(date -d tomorrow +\%d)" = "01" ] && command). In EventBridge or Spring, writing L in the day-of-month field is all it takes. If you write 31, it won’t run in months without a 31st.
Why are cron’s day of month and weekday ORed rather than ANDed?
It’s a rule inherited from early Unix cron: specifying both is defined as OR so that something like “the 1st and 15th of every month, and every Friday too” fits in one line. For compatibility, Linux cron, GitHub Actions and Kubernetes still read it the same way. Spring and systemd use AND.
Can cron run every other week (once every two weeks)?
Not with the expression alone. Run it weekly, then either check in the command whether the week number is even or odd (look at the remainder of date +\%V divided by 2), or record the last run date and check that 14 days have passed. */14 in the day-of-month field just means the 1st, 15th and 29th, not every two weeks.

Tools in this article

Related articles

How-to Guide10 min read

Running GitHub Actions cron on Japan Time: UTC Conversion Table and Fixing Day Shifts and Delays

You wrote “0 9 * * 1-5” in your GitHub Actions schedule, but the workflow ran at 6 p.m. Japan time. That isn’t a typo: GitHub Actions always reads cron expressions in UTC (Coordinated Universal Time). This article covers how to convert a Japan Standard Time (JST) schedule into a UTC cron expression, with a ready-made table; why times between midnight and 8:59 a.m. shift the weekday and date back by one; how to split a schedule into two lines when one won’t do; and what to do when a schedule runs late or not at all.

Read article →
How-to Guide10 min read

How to Write crontab, with a Cheat Sheet: The Five Fields, Symbols, Common Examples and Commands

Recurring jobs like “back up every day at 3:00” or “run a monitoring script every 5 minutes” take a single line in crontab on Linux. But the five fields (minute, hour, day of month, month, day of week) and the symbols * , - / are easy to misread at first, and accidents are common: a job that runs 60 times an hour, or one meant to run every 7 minutes that ends up with a 4-minute gap. This article walks through the basics of crontab syntax and what each symbol means, a cheat sheet you can copy from, how to use crontab -e, -l and -r, and what to watch out for when writing the command part, mostly through examples.

Read article →
How-to Guide10 min read

How to Write EventBridge cron Expressions: What ? Means, Weekday Numbers, Japan Time and Converting from crontab

EventBridge cron expressions, used on AWS to run Lambda functions or ECS tasks on a schedule, look like Linux crontab but differ in the number of fields, the use of ?, the weekday numbers and the time zone. Write “0 9 * * 1-5” the crontab way and you get an error; copy the weekday numbers across and everything shifts by a day; and times are read in UTC, so you’re 9 hours off from Japan time. This article covers the six fields of an EventBridge cron expression and the ? rule, the weekday numbers, how to use L, W and #, how rate expressions differ, how Japan time works in rules versus Scheduler, and a conversion table for crontab, Kubernetes, Spring and systemd.

Read article →