Skip to main content
Tools Harbor

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

PlatformExpression
AWS EventBridgecron(0 0 L * ? *)
Quartz (Java)0 0 0 L * ? *
Linux crontab0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /script.sh
Kubernetes CronJob0 0 28-31 * * (test inside the container)
GitHub ActionsSame 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