A cron expression that runs once per month:
0 0 1 * *
Field by field: minute 0, hour 0, day-of-month 1, every month, every day-of-week. The job fires at midnight at the start of every month. 12 fires per year. Page above generates this and other monthly variants; this companion explains the cadence and its specific edge cases.
Common monthly variants
| Expression | When it fires |
|---|---|
0 0 1 * * | Midnight on the 1st of every month |
0 3 1 * * | 03:00 on the 1st of every month (off-peak) |
0 0 15 * * | Midnight on the 15th |
0 0 1,15 * * | The 1st and the 15th (twice a month) |
0 0 L * * | Last day of the month — non-standard, see below |
0 0 1 1,4,7,10 * | Quarterly — 1st of January, April, July, October |
@monthly | Equivalent to 0 0 1 * * (Vixie cron and most modern schedulers) |
The “last day of the month” problem
There’s no clean way in standard Vixie cron to say “the last day of every month.” The day-of-month field accepts only fixed values 1–31; it doesn’t know that February ends on the 28th (or 29th). Workarounds:
Lsyntax is supported by some schedulers (Quartz, AWS EventBridge, some cron extensions).0 0 L * *means last day. Not supported by Vixie cron.- Run on day 1 of the next month and act on the previous month. Often the cleaner solution — the job runs at a fixed schedule and computes “yesterday’s date” or “last month” inside.
- Run every day in the last week and check inside the script.
0 0 28-31 * *fires every day from the 28th onward; the script checks iftomorrowis a different month and exits otherwise.
The 0 0 31 * * mistake
0 0 31 * * does not run “on the last day of the month.” It runs only on the 31st, which exists in 7 months: January, March, May, July, August, October, December. February, April, June, September, November have no 31st, so the job is skipped those months.
If you want quarterly runs on day 31, this works (the relevant months all have 31 days). If you want monthly, this doesn’t.
Day-of-month and day-of-week interaction
For a monthly job, the day-of-week field stays as *. If you set both:
0 0 1 * 1 ← midnight on the 1st OR every Monday
This is the well-known cron quirk: when both fields are non-*, most cron implementations treat them as OR, not AND. The expression above fires at midnight on the 1st AND midnight every Monday — about 5–6 times a month, not once.
For a strictly monthly job, leave day-of-week as *.
When monthly is the right cadence
- Bills, invoices, statements. End-of-month or beginning-of-month billing runs.
- Monthly reports. Roll up the previous month’s data and email it.
- Compliance / audit batches that have to run on a known monthly cadence.
- Cleanup of monthly partitions. Drop the table partition that’s now beyond retention.
- Renewal jobs that touch every active subscription on its monthly anniversary — though this often needs per-row scheduling, not a single monthly cron.
When monthly might not be enough
If the job has to run on the user’s local “1st of the month” and your users span timezones, a single monthly cron in UTC can fire as late as 11 PM the previous day in Pacific time or as early as 8 AM the next day in Asia. Either:
- Run hourly and check per-user state, or
- Spawn per-timezone schedules.
A single UTC monthly fire isn’t sufficient when the boundary matters per-customer.
Common pitfalls
- Day-31 only. Discussed above.
- Forgetting weekdays. “First Monday of the month” can’t be expressed in standard cron. Run daily with a weekday check inside the script:
0 0 1-7 * 1fires at midnight on days 1–7 of the month, but only when the day is also Monday — which under the OR rule is wrong (fires every weekday, plus days 1–7 regardless of weekday). The correct way is0 0 1-7 * *plus an inside-the-script check. - DST around midnight on day 1. If day 1 happens to be a DST transition day (rare but possible — most DST transitions are mid-month, but not always), the midnight fire is subject to the DST quirks discussed in cron daily.
- Long-running monthly jobs. A month-end aggregation that takes hours to run blocks the next month from starting. Schedule monthlies several hours ahead of when the result is needed.
- Assuming “the 1st” is “the start.” For some businesses the financial month closes on the 5th of the next calendar month, and a
0 0 1 * *cron is a few days too early.
For other cadences, see cron weekly, cron daily, cron expression generator. For reading a monthly expression in plain English, cron parser.