Cron expression for the last day of the month
The cron expression for “last day of the month” depends on which scheduler you’re using. AWS EventBridge and Quartz Scheduler support the L operator — cron(0 0 L * ? *) runs at midnight on whatever the last day of the current month happens to be. Unix cron, Kubernetes CronJobs and GitHub Actions don’t have L and need a workaround using a 28–31 day range plus a shell test.
Quick reference
| Platform | Expression |
|---|---|
| AWS EventBridge | cron(0 0 L * ? *) |
| Quartz (Java) | 0 0 0 L * ? * |
| Linux crontab | 0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /script.sh |
| Kubernetes CronJob | 0 0 28-31 * * (test inside the container) |
| GitHub Actions | Same workaround as Linux |
Why doesn’t Unix cron support “last day of month”?
Standard 5-field Unix cron has no operator for “last day”. The L extension comes from Quartz Scheduler (the Java cron library), and AWS EventBridge adopted it directly. Linux/macOS cron, Kubernetes CronJobs and GitHub Actions all run vanilla Unix cron syntax, so a literal L won’t parse — you’ll see a syntax error or, worse, the entry will be silently ignored.
The community has settled on one workaround: schedule the job on days 28, 29, 30 and 31 (all the candidates for “last day”), then have the command itself check whether tomorrow is the 1st of the next month before doing real work.
The 28-31 + date -d tomorrow workaround
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/eom-job.sh
Read it left to right:
0 0 28-31 * *— fire at midnight on every day in the range 28–31[ "$(date -d tomorrow +%d)" = "01" ]— test: is tomorrow the 1st of the next month?&& /usr/local/bin/eom-job.sh— run the actual job only if the test passed
On January 28, the test runs but tomorrow is January 29 — skip. On the 29th, tomorrow is the 30th — skip. On the 30th, tomorrow is the 31st — skip. On the 31st, tomorrow is February 1 — fire. Net result: exactly one fire per month, on whatever day happens to be last.
The same logic handles February (28 or 29), April / June / September / November (30 days) and the 31-day months automatically — date -d tomorrow does all the calendar math.
Note the escaped \% in crontab: bare % is treated as a newline by crontab -e and will break the entry. The escape is only required when writing to a crontab file directly; it’s not needed inside YAML command: arrays or shell scripts.
On AWS EventBridge: just use L
cron(0 0 L * ? *)
L in the day-of-month field means “last day of the month”. You can also offset: cron(0 0 L-3 * ? *) runs three days before the last day — useful for end-of-month batch reports that need to settle before the next month rolls over. The day-of-week field must be ? because AWS doesn’t allow both day-of-month and day-of-week constrained at the same time.
See AWS EventBridge cron expressions for the full ruleset and other extensions like # (nth weekday).
On Kubernetes CronJob
Kubernetes runs Unix cron syntax, so no L. The cleanest pattern is to fire every day in the range and test inside the container:
apiVersion: batch/v1
kind: CronJob
metadata:
name: end-of-month
spec:
schedule: "0 0 28-31 * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: eom
image: my-org/eom-job:1.0
command: ["/bin/sh", "-c"]
args:
- |
if [ "$(date -d tomorrow +%d)" = "01" ]; then
/app/run.sh
fi
The pod spins up on days 28–31 regardless. Most days the test fails and the container exits cleanly without doing real work — that’s a small amount of waste in exchange for not maintaining a calendar table. The Kubernetes CronJob schedule generator emits a complete manifest with concurrencyPolicy, activeDeadlineSeconds and timeZone already filled in.
Common mistakes
Forgetting the && makes the test useless. [ ... ] ; /script.sh runs the script every night 28–31 regardless of the test result. It must be && (run only if test succeeded) or wrapped in an explicit if block.
Assuming day-31 alone covers it. 0 0 31 * * skips February (28/29), April, June, September and November (30 days each). Six of twelve months never fire. The 28–31 range is necessary.
Using \%d outside crontab files. The \% escape only matters inside crontab -e. YAML command: arrays, shell scripts, and cron.d/ files take the bare %d.
On AWS: forgetting ? in day-of-week. cron(0 0 L * * *) is rejected — the * in dow conflicts with L in dom. Use ? for whichever field is unconstrained.
For ready-to-paste expressions for other schedules, see common cron schedules, or build a custom expression with the Cron Expression Builder.
Frequently asked questions
- Why doesn't Unix cron have an `L` operator?
- Unix cron predates the `L` operator. `L` originated in Java's Quartz Scheduler and was adopted by AWS EventBridge. Standard 5-field Unix cron (Linux, macOS, Kubernetes CronJobs, GitHub Actions, GitLab CI) doesn't support it — for last-day-of-month semantics on those platforms you need the 28–31 + `date -d tomorrow` workaround in the command itself.
- How do I run on the second-to-last day of the month?
- In AWS EventBridge or Quartz, use `cron(0 0 L-1 * ? *)`. In Unix cron, schedule on days 27–30 and check `date -d "2 days"` for `01`: `0 0 27-30 * * [ "$(date -d \"2 days\" +\%d)" = \"01\" ] && /your/script.sh`.
- Will the schedule fire correctly on Feb 28 in non-leap years and Feb 29 in leap years?
- Yes — the workaround tests whether tomorrow is the 1st of the next month, so the calendar math is delegated to `date`. On Feb 28 in a non-leap year, tomorrow is March 1 → fire. In a leap year, Feb 28 → Feb 29 (skip), Feb 29 → March 1 (fire). Either way, exactly one fire per month.
Need a different schedule?
Build cron expressions for Unix, Kubernetes, AWS EventBridge and Quartz — with a human-readable description and the next 5 run times.
Open the Cron Expression Builder →Related
What is a cron job?
A cron job is a scheduled command that runs at fixed times on Unix-like systems. The schedule is a one-line cron expression of five space-separated fields.
Cron expression for every 5 minutes
The cron expression `*/5 * * * *` runs every 5 minutes in Linux crontab, Kubernetes, GitHub Actions; `cron(*/5 * * * ? *)` in AWS EventBridge.
Cron expression for every 15 minutes
The cron expression `*/15 * * * *` runs every 15 minutes — at :00, :15, :30, :45 — across Linux, Kubernetes, GitHub Actions and AWS.
Cron expression for every 30 minutes
The cron expression `*/30 * * * *` runs every 30 minutes — at :00 and :30 of every hour. Same in Linux, Kubernetes and GitHub Actions; `cron(*/30 * * * ? *)` in AWS.
Cron expression for every hour
The cron expression `0 * * * *` runs once per hour on the hour. Same syntax in Linux, Kubernetes and GitHub Actions; `cron(0 * * * ? *)` in AWS EventBridge.
Cron expression for daily at midnight
The cron expression `0 0 * * *` runs once daily at midnight. Watch the timezone — Kubernetes < 1.25 and AWS EventBridge default to UTC, not local time.