Cron last day of the month: why 31 fails and what to use instead
By TechCompare · Updated · How we check
Pair 0 0 28-31 * * with a shell test that passes only when tomorrow is the 1st. If a run on the 1st of the next month can do the job, use 0 0 1 * * instead and skip the test.
Calculator
Next runs of 0 0 28-31 * *
Example from Mon, Nov 9, 2026 at 8:00 AM.
- Mon, Nov 30, 2026 at 12:00 AM
- Thu, Dec 31, 2026 at 12:00 AM
- Sun, Jan 31, 2027 at 12:00 AM
- Sun, Feb 28, 2027 at 12:00 AM
- Wed, Mar 31, 2027 at 12:00 AM
Only the last day of each month counts, like the shell test on the crontab line.
0 0 28-31 * * runs at midnight on days 28 to 31 of every month, which always includes the last day. It also fires on the earlier days in that range, so most months see runs on days that aren't the last day. This page explains how to filter those days out, gives the macOS and FreeBSD version, and shows the systemd line that skips the problem.
How this is calculated
Plain cron has no last day field, so the first try is usually 0 0 31 * *. That line works in every 31-day month and skips the rest. April, June, September, and November never reach a 31st, and neither does February. Cron won't warn you. It's easy to miss, because the job looks fine for most of the year. The fix is 0 0 28-31 * *, which covers every possible last day. Each month has exactly one last day, so exactly one of those days passes the test. The crontab line runs that test:
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /path/to/jobRead the fields left to right. 0 0 sets midnight. 28-31 limits the runs to the final days of each month, and the two * fields allow every month and every weekday. The test picks the one real last day. date -d tomorrow +\%d prints tomorrow's day of the month, which is 01 only when today is the last day. The && runs the job only when the test passes, so the job runs once a month, on the real last day. Because the test asks the calendar, the same line handles every month length and leap years.
Two details break this line if you miss them. First, % is special in a crontab. crontab(5) says a % "will be changed into newline characters" unless it's escaped, and everything after it goes to the command as input, so the line won't run as written. That's why the test has \%d. A script file doesn't need the backslash. Second, macOS and FreeBSD use BSD date, which has no -d tomorrow form. It adjusts the date with -v, so the test becomes [ "$(date -v+1d +\%d)" = 01 ] && /path/to/job. The FreeBSD date(1) man page says a plus or minus sign before the value moves the date forward or backward. This form hasn't been run on a BSD system, so check it with date -v+1d by hand before you rely on it.
If the job works on last month's data, you may not need the test. Run it on the 1st with 0 0 1 * * and have it handle the month that just ended. Every month has a 1st, so the short month problem never comes up. The cost is timing. The job runs after the month closes, so a job that has to act on the last day itself needs the test. The monthly schedule page covers that pattern in more detail.
systemd timers don't need a test, because ~ counts back from the end of a month. *-*~01 means the last day of every month, and systemd-analyze calendar lists the real month ends for it. The systemd.time(7) man page documents the ~ form, with *-02~03 meaning the third last day in February. Put this line in a timer unit:
OnCalendar=*-*~01 00:00:00Cron and date both use the server's time zone by default, so they read the same clock. If the server runs on UTC and you're somewhere else, read the time zone pitfalls page before you pick the hour. The job also runs at 00:00, the first minute of the last day, so a job that totals up "today" sees an empty day. If it needs the whole day, use 59 23 for the minute and hour. The test still works at that hour, because tomorrow is still the next calendar day.
Verdict
Don't replace the test with a hand-written list of month lengths. A list is easy to get wrong in February, and the calendar already knows the answer. To see the logic work, run this in a terminal on the last day: [ "$(date -d tomorrow +%d)" = 01 ] && echo last day. It prints last day only on that day. The check covers the logic only, so it won't catch a missing backslash in the crontab.
More Cron scenarios
Related guides
Frequently asked questions
How do I run a cron job on the last day of every month?
0 0 28-31 * * with the test: [ "$(date -d tomorrow +\%d)" = 01 ] && /path/to/job. The test passes only when tomorrow is the 1st, so the job runs on the real last day. On systemd, OnCalendar=*-*~01 00:00:00 does the same job without a test.Why doesn't the 31st run every month?
Does the test handle February and leap years?
Does date -d tomorrow work on macOS?
-d tomorrow form is GNU date, which Linux uses. macOS uses BSD date, which adjusts the day with -v, so the line becomes [ "$(date -v+1d +\%d)" = 01 ] && /path/to/job. That form hasn't been run on a BSD system, so try date -v+1d by hand first.Is it better to run on the 1st instead?
0 0 1 * *, which needs no test and has no short month problem. A job that must act on the last day itself, like a payment run, needs the test. The monthly schedule page has more on the 1st pattern.Related tools
Unix Timestamp
Convert between Unix timestamps and human-readable dates.
Use toolCHMOD Configurator
Calculate Linux file permissions using checkboxes or octal numbers, then view the matching symbolic notation.
Use toolText Encoding Converter
Convert text to Base64, binary, hex, URL encoding and HTML entities, and back again.
Use tool