Cron schedule
Cron on the last day of the month
Standard 5-field cron has no syntax for the last day of the month. Schedule 0 0 28-31 * * and let the command run only when tomorrow is the 1st; Quartz, Spring, node-cron and Cloudflare accept L directly, as in 0 0 L * *.
| Expression | 0 0 28-31 * * |
|---|---|
| Runs | 12 times in 2027 |
| systemd OnCalendar | *-*~01 00:00:00 |
How it works
Months end on the 28th, 29th, 30th or 31st, and the day-of-month field can only list fixed numbers. 0 0 31 * * misses five months; 0 0 28-31 * * fires up to four times at the end of every month. POSIX and Vixie cron never added a “last” keyword.
The portable fix is to fire on every candidate day and let a one-line test decide: if tomorrow’s day number is 01, today is the last day. In a crontab line the % in date +%d must be written \%, or cron cuts the command there.
Quartz introduced L in the day-of-month field, and it has spread: Spring’s CronExpression, node-cron 4.x and Cloudflare Cron Triggers all accept it, and systemd has its own form, *-*~01. The next-runs table below evaluates the L form.
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 | minute 0 (on the hour) |
| Hour | 0 | hour 0 (12 AM) |
| Day of month | 28-31 | days 28 to 31 |
| Month | * | every month |
| Day of week | * | every day of the week |
The table describes the portable part, 0 0 28-31 * *. The next-run times below use 0 0 L * *, the Quartz-style form, which fires only on the last day.
Next runs
As on GitHub Actions without timezone, Vercel, Cloudflare and Kubernetes with timeZone "Etc/UTC".
| Run | Your time | UTC |
|---|---|---|
| 1 | Calculating… | |
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
Ready-to-paste versions
The same schedule for each scheduler, with what differs on each one.
crontab (Linux, macOS, BSD)
# Days 28-31, but only act when tomorrow is the 1st (GNU date):
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/job.sh
# BSD / macOS date:
0 0 28-31 * * [ "$(date -v+1d +\%d)" = "01" ] && /usr/local/bin/job.sh%has to be escaped as\%inside a crontab line; an unescaped%ends the command and the rest goes to its stdin. The check runs at most four times a month and does nothing on three of them.
GitHub Actions
on:
schedule:
- cron: '0 0 28-31 * *'
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Only on the last day of the month
id: last
run: echo "is_last=$([ "$(date -u -d tomorrow +%d)" = 01 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
- if: steps.last.outputs.is_last == 'true'
run: ./job.sh- GitHub uses POSIX cron syntax with no
L, so the workflow wakes up on days 28 to 31 and a step checks the date. No%escaping here: that rule is crontab-only.
Kubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: month-end
spec:
schedule: "0 0 28-31 * *"
timeZone: "Etc/UTC"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: job
image: alpine:3.20
command: ["/bin/sh", "-c"]
args:
- |
[ "$(date -d @$(( $(date +%s) + 86400 )) +%d)" = "01" ] || exit 0
exec /app/job.sh- Kubernetes’ schedule syntax has no
L. The container checks whether tomorrow is the 1st and exits 0 otherwise; the arithmetic form ofdate -d @…works in BusyBox (Alpine) as well as GNU date.
Vercel Cron Jobs
{
"crons": [
{ "path": "/api/month-end", "schedule": "0 0 28-31 * *" }
]
}- No
Lon Vercel. Schedule days 28-31 and have the function return early unless tomorrow (in UTC) is the 1st. That’s up to four runs a month, one a day, which the Hobby plan allows (per-hour precision there).
Cloudflare Workers
[triggers]
crons = ["0 0 L * *"]- Cloudflare supports Quartz-style
Lin the day-of-month field, so no workaround is needed.LWwould be the last weekday of the month. Times are UTC.
node-cron (Node.js)
import cron from 'node-cron';
// L = last day of the month (node-cron 4.x)
cron.schedule('0 0 0 L * *', async () => {
await runMonthEnd();
}, { timezone: 'UTC' });- node-cron 4.x accepts
L(andL-3,LW) in the day-of-month field.
systemd timer
# /etc/systemd/system/job.timer
[Unit]
Description=Run job.service on the last day of each month
[Timer]
OnCalendar=*-*~01 00:00:00
Persistent=true
[Install]
WantedBy=timers.target
# /etc/systemd/system/job.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/job.sh
# systemctl daemon-reload && systemctl enable --now job.timer- Check it with
systemd-analyze calendar '*-*~01 00:00:00', which prints the normalized form and the next elapse.AccuracySecdefaults to 1min, so the start can drift by up to a minute. Persistent=trueruns the job at boot if the machine was off at the scheduled time, which plain cron doesn’t do.- The
~means “counting from the end of the month”:*-*~01is the last day,*-*~02the one before it.
Spring @Scheduled and Quartz
@Scheduled(cron = "0 0 0 L * *", zone = "UTC")
public void monthEnd() { }
// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 L * ?")- Spring (5.3+) and Quartz both support
Lin the day-of-month field;L-2is two days before the last day andLWthe last weekday.
Pitfalls
0 0 31 * * runs seven times a year
Only January, March, May, July, August, October and December have a 31st. cron doesn’t move a run to the nearest existing day; it just doesn’t fire.
The unescaped %
date +%d works in a shell and silently breaks in a crontab, where % means newline. Write date +\%d in the crontab line, or move the logic into a script file.
Consider the 1st instead
Many month-end jobs are really “after the month closes” jobs. Running 0 0 1 * * and processing the previous month avoids the last-day problem entirely and sees the complete last day of data.
Frequently asked questions
- How do I run a cron job on the last day of the month?
- Schedule 0 0 28-31 * * and prefix the command with [ "$(date -d tomorrow +\%d)" = "01" ] &&. Where L is supported (Quartz, Spring, node-cron, Cloudflare), use L in the day-of-month field.
- Does crontab support L for the last day?
- No. cronie, Debian cron and other Vixie-derived crons don’t support L. It comes from Quartz.
- What is the last day of the month in systemd?
- OnCalendar=*-*~01 00:00:00. The tilde counts days from the end of the month.
- How do I run on the last weekday of the month?
- Use LW in Quartz, Spring, node-cron or Cloudflare. In plain cron, run on days 26-31 on weekdays and check in the script that no later weekday remains in the month.
Last reviewed by Arielton Oberek.