Run a cron job on the last day of the month
Use a calendar guard for traditional cron, or a systemd calendar expression that handles month ends directly.
Traditional five-field system cron has no portable “last day of the month” field. Schedule a small check on days 28 through 31 and run the real job only when tomorrow is the first. This naturally handles short months and leap years.
On a Linux system with GNU date, this user-crontab entry runs at midnight on the month's final day:
0 0 28-31 * * [ "$(date -d 'tomorrow' +\%d)" = 01 ] && /opt/jobs/month-end.sh
This is the start of the last day, not the instant after that day's business has finished. For a close that needs the entire month's data, consider running on the first day of the next month and processing the previous period.
How the check works
The candidate dates are 28, 29, 30 and 31; dates that do not exist simply never match. On those that do, date supplies tomorrow's day number. Only the final date in the month yields 01.
GNU date -d is not portable to BSD/macOS date, which uses different options. Keep the scheduler's timezone and the date calculation's timezone aligned. A TZ override in the command can otherwise make the date guard disagree with cron's calendar.
Escape percent signs in crontab
Cron processes unescaped % in a command specially, even inside shell quotes. That is why the format above uses +\%d. Inside an ordinary shell script, use +%d instead.
Putting the guard into a script makes it easier to test and avoids cron-specific percent escaping:
#!/bin/sh
tomorrow_day=$(date -d 'tomorrow' +%d) || exit 1
if [ "$tomorrow_day" = 01 ]; then
exec /opt/jobs/month-end.sh
fi
Schedule this wrapper on days 28–31. The if form exits successfully when the guard does not match, so an intentional skip is not reported as a failed job by a status-based monitor. A failed date calculation exits nonzero instead of being mistaken for a normal skip.
Use a systemd calendar timer
On a compatible systemd host, this timer expression names the last day directly:
[Timer]
OnCalendar=*-*~1 00:00:00
This is a fragment: pair it with a service and install/enable the timer. Validate the expression on the target version:
systemd-analyze calendar '*-*~1 00:00:00'
~1 means the final day; ~3 means the third-to-last day. For a calendar timer, Persistent=true can trigger a catch-up activation after downtime, but does not replay every missed period. Make the job's period selection and duplicate handling explicit.
Verify the period, not just the trigger
Record the business month, start and completion time, and result. Check leap-year February and 30- and 31-day months in tests. If no success record appears, distinguish a missed trigger, an intentional guard skip, job failure and missing log collection.
Common questions
Does this work on February 28? In a non-leap year, yes. In a leap year the guard matches February 29 instead.
What about the Nth day from the end? For small fixed N, a script can test whether the date N days ahead is the first of the next month; it must be scheduled across all possible candidate dates. Do not simply change the comparison to day N or keep 28-31 for earlier month-end dates. A compatible systemd ~N expression is clearer.
Why did the command appear truncated? Check unescaped percent signs first, then inspect the actual command in cron's launch logs.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan