Last day of the month (the safe way)
Cron has no native 'last day' syntax. This fires on the 28th through 31st and relies on a guard clause to no-op on any day but the real last one.
Last updated
In plain English
- Fires at 23:55 on the 28th, 29th, 30th and 31st
- Needs an in-script guard to isolate the true last day
Why it’s written this way
28-31 in the day-of-month field is a range: the job launches on every one of those four dates, every month, at 23:55. That's deliberate — it's the only portable way to guarantee a hit on the true last day, since months vary between 28 and 31 days long.
The schedule alone can't finish the job: the actual 'is today the last day' decision has to live in the script itself, guarding the body with something like [ "$(date -d tomorrow +%d)" = "01" ] || exit 0 so only the genuine last-day run does anything.
Edge cases to know
- →Skip the guard clause and the job runs its full logic on the 28th, 29th, 30th AND 31st in any month with 31 days — for anything non-idempotent, like charging a card or sending an invoice, that's up to three unwanted extra runs.
- →Some schedulers support smarter 'last day' syntax directly — Vixie cron's L is nonstandard and rarely available, but systemd timers' OnCalendar do support it. Prefer the native option if the target system has one.
- →The guard shown here is GNU date syntax (date -d); BSD and macOS date need a different flag (date -v+1d), so the exact one-liner isn't portable across every box the job might end up running on.
Part by part
- 55 23
- Five minutes to midnight, so the run lands inside the month it is closing rather than after it has ended.
- 28-31
- The candidate days. Cron cannot express "the last day", so the line fires on all four and the script decides which one is real.
Common mistakes
55 23 31 * *Only months with 31 days have that date, so this silently skips February, April, June, September and November. Seven runs a year rather than twelve, with no error to indicate the omission.
55 23 28-31 * *55 23 28-31 * * without a guard in the scriptThe schedule fires up to four times per month, so a job that does not check whether tomorrow is the first will run repeatedly — closing the books four times in a row.
55 23 L * *The L specifier means last day in Quartz and several commercial schedulers, and is not part of Vixie cron. On a standard Linux crontab it is a syntax error rather than a clever shortcut.
Questions
What guard should the script use?
Check whether tomorrow is the first of a month — date -d tomorrow +%d equals 01 on the last day and nothing else. It is one line, it works in every month, and it needs no table of month lengths.
Why not just run on the first instead?
Often you should, and it is simpler. This pattern exists for work that must complete within the period it reports on, such as a cut-off that cannot include the next day's transactions.
Does 28-31 cause a problem in February?
No. Cron simply does not fire on dates that do not exist, so a non-leap February gets one run on the 28th, which the guard then confirms is the last day.
Is five minutes of headroom enough?
For a quick job, yes; for anything slower it is not, because a run that overruns midnight is doing month-end work on the wrong date. Start earlier, or have the script pin the reporting date at launch rather than reading it as it goes.