Quick answer
Standard cron has no value for the last day of the month, so the usual expression is 0 0 28-31 * * with a guard in the command that only continues when tomorrow is the 1st. Cron starts the job on every candidate day from the 28th to the 31st, and the guard lets only the real last day through. Cloudflare Workers, AWS EventBridge and Quartz accept L for the last day instead.
- Live next runs
- Any time zone
- Field-by-field breakdown
- Seven platform snippets
- Daylight saving aware
How to use this schedule
- 1Copy
0 0 28-31 * *with the Copy expression button. - 2Check Next runs in your time zone to confirm it fires when you expect, including around daylight saving changes.
- 3Pick your platform under Use it on your platform and copy the ready-made crontab line, workflow, manifest or timer.
- 4Need a different time or day? Click Customize to open it in the cron expression generator and change any field.
How do I run a cron job on the last day of the month?
Months end on the 28th, 29th, 30th or 31st, and the five standard cron fields can't say "whichever comes last". 0 0 31 * * skips five months a year, and 0 0 L * * is rejected by Linux crontab. The portable answer is to schedule every possible last day and filter in the command:
# crontab: last day of the month at 00:00 (GNU date; escape % as \% in crontab)
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /path/to/jobdate -d tomorrow +%d prints tomorrow's day of the month. Only on the real last day is that 01, so the && lets the job run exactly once a month. On macOS and BSD, date uses a different flag: date -v+1d +%d. Inside a script file, write a plain %d; the backslash is only needed in the crontab line itself, where an unescaped % turns the rest of the line into standard input.
Which schedulers support L for the last day?
Several schedulers extend cron with L (last) in the day-of-month field, so no guard is needed:
| Scheduler | Last day of the month at 00:00 |
|---|---|
| Cloudflare Workers Cron Triggers | 0 0 L * * |
| AWS EventBridge | cron(0 0 L * ? *) |
| Quartz and Spring (seconds first) | 0 0 0 L * ? |
| systemd timer | OnCalendar=*-*~01 00:00:00 |
| Linux crontab, Kubernetes, GitHub Actions, Vercel | Not supported: use 28-31 plus a guard |
If you control the job, there is an even simpler design: run at 00:00 on the 1st and process the month that just ended. The data is complete, there is no guard to get wrong, and the schedule is the plain 0 0 1 * * used for every month, which every scheduler understands.
What does each field in 0 0 28-31 * * mean?
| Field | Value | Allowed | Meaning |
|---|---|---|---|
| Minute | 0 | 0–59 | at minute 0 (on the hour) |
| Hour | 0 | 0–23 | at hour 0 (midnight) |
| Day of month | 28-31 | 1–31 | days 28 through 31 of the month |
| Month | * | 1–12, JAN–DEC | every month |
| Day of week | * | 0–7 (0 and 7 are Sunday), SUN–SAT | every day of the week |
Read together: At 00:00 on the last day of each month, whether that is the 28th, 29th, 30th or 31st.
The day-of-month range 28-31 only narrows the candidates; a date check in the command (or L on platforms that support it) picks the real last day.
How do I adjust 0 0 28-31 * * to my own schedule?
Small edits to 0 0 28-31 * * cover most real-world needs:
| Expression | What it does |
|---|---|
0 0 L * * | Last day of the month on Cloudflare Workers (not valid in crontab) |
0 0 1 * * | The 1st at midnight, processing the month that just ended |
59 23 28-31 * * | With the same guard, runs in the final minute of the month |
0 0 31 * * | Not a last-day schedule: only the seven months that have a 31st |
0 0 28,29 2 * | The candidate last days of February |
Need something else? Click Customize at the top of the page to open this schedule in the cron expression generator and change any field.
Related schedules: First Monday of the month · Every month · Every quarter · Every year
Browse all schedules in the cron schedule library.
How do I run 0 0 28-31 * * in crontab, GitHub Actions, Kubernetes and the cloud?
| Platform | Write it as | Time zone |
|---|---|---|
| Linux crontab | 0 0 28-31 * * + date check | Server's time zone; CRON_TZ= in cronie |
| GitHub Actions | - cron: "0 0 28-31 * *" + date check | UTC by default; add timezone: (IANA name) |
| Kubernetes CronJob | schedule: "0 0 28-31 * *" + date check | Set timeZone: (1.27+) |
| Cloudflare Workers | 0 0 L * * | UTC only; supports L and # (Monday is 2) |
| Vercel Cron Jobs | 0 0 28-31 * * + date check | UTC only; more than once a day needs Pro (Hobby: once a day) |
| AWS EventBridge Scheduler | cron(0 0 L * ? *) | Takes a time zone; legacy scheduled rules are UTC only |
| systemd timer | OnCalendar=*-*~01 00:00:00 | Add a zone after the time, e.g. Europe/London |
In a crontab (crontab -e), the full line looks like this:
# Set TZ to the zone your calendar runs in; CRON_TZ doesn't reach the command.
0 0 28-31 * * [ "$(TZ=UTC date -d tomorrow +\%d)" = 01 ] && /path/to/job.shThe live panel at the top of the page builds complete, copy-ready files for each platform (workflow, CronJob manifest, wrangler.toml, vercel.json, AWS CLI and systemd units) and can shift the schedule to UTC for platforms that need it.
What can go wrong with 0 0 28-31 * *?
- In a crontab line,
%must be written\%. Without the backslash, cron cuts the command at the first%and the guard silently breaks. date -d tomorrowis GNU date syntax. macOS and BSD usedate -v+1d, and minimal container images may ship a differentdate, so test the guard where the job actually runs.- The guard uses the server's time zone. At 00:00 UTC on the 31st it is still the 30th in the Americas, so set
TZ=for thedatecommand if your month is defined in another zone. - At 00:00 on the last day, that day has only just started. If the job should see the whole month, run on the 1st instead, or use
59 23with the same guard. - On UTC-only platforms (Cloudflare Workers, Vercel, legacy EventBridge rules), the local time this runs at moves by an hour when daylight saving time starts or ends. Use GitHub's
timezone:, KubernetestimeZone, EventBridge Scheduler or cronie'sCRON_TZto pin it to local time.
Frequently asked questions
Last updated by the Playxoft engineering team.