Crontab Once a Month: Exact Syntax, Pitfalls, and Monitoring
August 31, 2026


Running a crontab job once a month sounds simple until you hit the day-31 problem, the missing "last day of month" syntax, or a silent failure nobody catches for weeks. This guide covers the exact expression, the shortcut that works (and where it breaks), and why infrequent jobs deserve more monitoring attention than the hourly ones you probably already watch closely.
What "Once a Month" Means in Crontab Syntax
A standard crontab line has five time fields, in order: minute, hour, day-of-month, month, and day-of-week. Scheduling something "once a month" means setting the day-of-month field to a fixed value and leaving month and day-of-week open, so the job fires on that date every month regardless of weekday.
The canonical pattern:
0 0 1 * *
That's minute 0, hour 0, day-of-month 1, every month, every day-of-week — midnight on the 1st. The day-of-week field stays a wildcard because you're targeting a date, not a weekday. If you're still shaky on how the five fields interact, the crontab basics tutorial covers that ground in more depth — this article assumes that foundation and focuses on monthly-specific detail.
Crontab Examples to Run a Job Once a Month
Copy-pasteable patterns for common monthly scheduling needs.
First of the month, midnight:
0 0 1 * *
First of the month, at 3:30 AM (avoids midnight batch congestion):
30 3 1 * *
A specific day, like the 15th, at 9 AM:
0 9 15 * *
Using the @monthly shorthand:
@monthly /path/to/script.sh
Pick the day-of-month value that matches your billing cycle, report deadline, or maintenance window, and set hour/minute to whatever suits your server's load.
The @monthly Shortcut: What It Does and Where It Falls Short
@monthly is a special string cron recognizes as identical to 0 0 1 * * — first minute of the first day of the month, every month. It's convenient shorthand and reads more clearly than five fields of numbers.
But it's rigid: it only ever means "midnight on the 1st." You can't use it for the 15th, a custom hour, or — critically — the last day of the month. For anything other than the very first moment of the month, use explicit five-field syntax instead. That limitation sets up the next set of problems.
Common Pitfalls: Short Months, Leap Years, and "Last Day"
The day-of-month field is where most monthly cron schedules quietly break. Set it to 31 and your job runs fine in January, March, May, July, August, October, and December — then simply doesn't fire in April, June, September, or November, since those months lack a 31st day. Cron doesn't warn you or roll the date forward; it just skips that month entirely. February compounds this: with 28 days most years and 29 in a leap year, a job pinned to day 29 or 30 will vanish from February's schedule almost every time.
There's no native crontab syntax for "last day of the month." The day-of-month field only accepts fixed numbers, ranges, steps, or wildcards — nothing that adapts to how many days a given month has. Some cron variants support an L token for this, but it's not portable, and standard crontab implementations on most Linux distributions don't recognize it.
The practical workaround is to schedule the job on each of the last few possible days — 28, 29, 30, and 31 — and have the script check whether tomorrow's date rolls into the next month:
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/script.sh
This runs the check daily from the 28th onward and only executes the real job when tomorrow is the 1st — simulating "last day of month" logic that crontab doesn't offer natively.
One more variable to nail down: which timezone is your cron daemon actually using? A job set to fire at midnight on the 1st might mean midnight UTC on a cloud server but midnight local time elsewhere, shifting which calendar day the job lands on for users in other regions. The deep dive on how the cron daemon actually works explains how the daemon resolves time — worth checking before you assume "midnight" means what you think.
Why Monthly Jobs Are the Riskiest to Monitor
An hourly job that fails gets 23 more chances to succeed, or fail loudly enough that someone notices, within the same day. A monthly job that fails gets one shot, then goes quiet for roughly 30 days before its next run. A job that only executes 12 times a year has a single point of failure per cycle, and if that point fails silently — a bad deploy breaks a script, a dependency changes, an API key expires — nobody may notice until a billing run doesn't generate an invoice or a monthly report never lands in someone's inbox.
This is why monthly cron job monitoring needs a different posture than monitoring frequent jobs. Grace-period alerting matters more: rather than flagging a run a few minutes late (which might just be normal system load), you want a system that knows a job due on the 1st and not checked in by the 2nd deserves an alert — because the next natural chance to catch it is a full month away. Execution history matters too, since with only 12 data points a year, seeing the pattern (did it run on the 1st last month? the month before?) is the only way to catch drift before it becomes a real incident.
Cronevra tracks every run of your scheduled HTTP jobs, keeps execution history so you can see exactly when a monthly job last succeeded, and sends grace-period alerts the moment a job misses its expected check-in — instead of leaving you to discover the failure a month later when a report or invoice doesn't show up.
Frequently Asked Questions
What is the crontab syntax to run a job once a month?
The standard expression is 0 0 1 * *, which runs at midnight on the 1st of every month. Set day-of-month to your target date and leave month and day-of-week as wildcards.
What does @monthly mean in crontab?
@monthly is a special string equivalent to 0 0 1 * * — it runs at midnight on the first day of every month. It's shorthand for readability but can't be customized for other days or times.
How do I schedule a cron job to run on the last day of every month?
Standard crontab has no native "last day of month" syntax. The common workaround is scheduling the job on days 28 through 31 and having the script check whether the next calendar day is the 1st, only running the real task on that match.
Can I run a cron job on a specific day, like the 15th, every month?
Yes — set the day-of-month field to 15 and leave month and day-of-week as wildcards, e.g. 0 9 15 * * to run at 9 AM on the 15th of every month.
Why didn't my monthly cron job run this month?
The most common cause is a day-of-month value that doesn't exist that month, such as 31 in April, June, September, or November. Timezone mismatches between what you expect and how the server resolves time can also shift the job to a different day than intended.
How do I get alerted if a monthly cron job fails or silently stops running?
You need monitoring with grace-period alerting and execution history, since a monthly job only reports in 12 times a year and a missed alert can go unnoticed for a full month. Cronevra tracks each run and flags missed or failed executions before the next cycle arrives.
A job that only fires once a month is unforgiving: miss one alert and you're operating blind for weeks. Set up execution history and grace-period alerts with Cronevra so a failed or skipped monthly run gets caught the same day it happens, not the next time your report or invoice fails to show up — check the pricing page to get started.