Cron Once a Month: Exact Syntax, Edge Cases & Monitoring
September 9, 2026


The Cron Expression for Once a Month
The exact expression for cron once a month is 0 0 1 * *. Reading the five fields left to right — minute, hour, day-of-month, month, day-of-week — this says "at minute 0, hour 0, on day 1, every month, any day of the week." In plain terms: midnight on the 1st, every month, forever.
Most crontab implementations also accept a shortcut: @monthly, which expands to exactly 0 0 1 * *. If you're still shaky on how the five fields interact or what characters like * and , mean in each position, work through Setting Up a Cron Job: The Complete Beginner's Guide before layering on the monthly-specific quirks below. For a deeper reference on flags and command syntax, see Cron Job Command: Syntax, Crontab Flags & Silent Failures.
Running on a Different Day of the Month
Swapping the day-of-month field lets you run on any date. To run on the 15th at midnight: 0 0 15 * *. To run at 9am on the 5th: 0 9 5 * *. The day-of-month field takes any integer from 1 to 31 — cron doesn't validate that the number makes sense for every month.
That's the trap. Schedule a job for the 31st with 0 0 31 * * and it runs fine in January, March, May, and every other 31-day month — but it's silently skipped in April, June, September, and November, and skipped again every February. A job meant to run once a month can end up running only seven times a year, with no error, no log entry, nothing. The same problem hits day 30 (skipped in February) and day 29 (skipped in every non-leap February). If your business logic assumes "once a month, every month," picking a fixed day above 28 without a fallback plan is a bug waiting to surface at the worst possible moment — say, a billing run that quietly doesn't fire in April.
Scheduling the Last Day of the Month
Standard Linux crontab (vixie-cron, the version most servers run) has no built-in concept of "last day of month." There's no wildcard for it — you have to build it yourself.
The common workaround is to schedule the job for every day from the 28th through the 31st, then have the script check whether tomorrow's date rolls into a new month:
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/script.sh
This runs the check daily from the 28th onward, but the script only actually executes on the true last day, whatever that day happens to be that month. It's a workaround, not a native feature — other schedulers handle this more gracefully. Quartz Scheduler and AWS EventBridge both support an L modifier (as in 0 0 L * *), meaning "last day of the month" natively. Classic crontab and vixie-cron do not support L at all — paste 0 0 L * * into a standard Linux crontab and it either errors out or gets silently misinterpreted depending on your cron implementation. Don't assume portability here; test on your actual target system.
Leap Years and Timezones: The Two Traps
February 29 is the leap-year trap. Schedule a job for day 29 without conditional logic and it runs only in leap years — 2024, 2028, 2032 — and is silently skipped every other February. If your job is meant to run monthly regardless of February's quirks, don't hardcode day 29 as your anchor; use the last-day-of-month pattern above, or pick an earlier day that exists in every month.
Timezone is the second trap, and it's easy to overlook because it's invisible until something breaks. Cron typically runs in the server's system timezone, which on most cloud infrastructure defaults to UTC. If your team is thinking in Eastern Time or Central European Time, "midnight on the 1st" in your head and on the server can be five, six, or more hours apart — which can push a run into the last day of the previous month, or delay it into the 2nd, depending on the offset direction. For a job that runs once a month, that's the difference between the report covering the right period or the wrong one. Always confirm TZ explicitly in the crontab or the job's environment rather than assuming it matches your local clock.
Why Monthly Jobs Are the Riskiest Schedule to Get Wrong
An hourly job that breaks gets caught within an hour or two, because someone notices the missing data almost immediately. A daily job that fails is usually flagged the next morning at the latest. A monthly cron job that fails has roughly 30 days to hide. Nobody's watching for it because nobody expects to check until the next scheduled run is due — and by then, a month of missing invoices, unsynced reports, or unrotated backups has already piled up.
This is what makes silent cron failure especially dangerous on a monthly cadence: the gap between "something broke" and "someone noticed" is the entire interval. A monthly job only gets about 12 attempts a year to prove it works, and each failure is expensive precisely because of how long it takes to surface. "Monthly cron job not running" is a common support search for exactly this reason — someone finally checks in week three or four and realizes the job hasn't fired since last month, or ran but errored out silently on line one.
Catching a Failed Monthly Run Before It Costs You
The fix isn't more discipline about checking logs — nobody reliably checks logs for a job that only runs 12 times a year. The fix is cron job monitoring that watches for you and speaks up the moment something's wrong.
Cronevra gives every scheduled job a cron execution history, so you can see at a glance whether last month's run actually completed, when it started, and how long it took — no digging through server logs. More importantly, it delivers cron failure alerts the instant a job fails or simply doesn't check in when expected, which is exactly the scenario a 30-day gap makes so easy to miss otherwise. For teams building out more schedules beyond the monthly one, 15 Cron Job Examples You Can Copy and Paste Today is a useful reference library.
Since a monthly job only gets about 12 chances a year to prove it's working, manual log-checking isn't a realistic safety net. Add your monthly job's URL to Cronevra and get execution history plus an alert the moment a run fails or doesn't check in — check Pricing to get started.
Frequently Asked Questions
What's the cron expression to run a job once a month?
The exact expression is 0 0 1 * *, which runs at midnight on the 1st day of every month. The five fields represent minute, hour, day-of-month, month, and day-of-week, with * meaning "every" value is accepted in that position.
Does @monthly work the same as 0 0 1 * * in crontab?
Yes, @monthly is a shorthand that expands to exactly 0 0 1 * * in standard cron implementations. It's safe to use as long as your specific cron daemon supports the @ shortcuts — most modern Linux systems do, but some minimal or embedded cron implementations only accept the five-field format.
How do I schedule a cron job for the last day of every month?
Standard Linux crontab has no native "last day" option, so the common workaround is 0 0 28-31 * * combined with a script check that only runs the command if tomorrow's date is the 1st. Systems like Quartz Scheduler and AWS EventBridge support an L modifier (0 0 L * *) that handles this natively, but vixie-cron and classic crontab do not.
Can I run a cron job on a specific weekday of the month, like the first Monday?
Standard crontab can't directly express "first Monday" in a single field, since day-of-month and day-of-week are evaluated independently. The typical approach is scheduling the job to check every Monday (0 0 * * 1) and adding a script condition that only proceeds if the date falls within the first 7 days of the month.
Why didn't my monthly cron job run this month?
The most common causes are scheduling a fixed day like 29, 30, or 31 that doesn't exist in every month, a timezone mismatch between server time and the time you expected, or a silent script failure that never surfaced because nobody checks a job that only runs once a month. Monitoring the job's execution history is the reliable way to catch which of these actually happened.
How do I handle February when scheduling a monthly cron job?
Avoid hardcoding day 29 as your monthly trigger, since it only exists in leap years and will silently skip every other February. Use the 28-31 last-day workaround, or anchor the schedule to an earlier day, such as the 1st or the 28th, that exists reliably in every month including February.