Run Jobs on the Last Day of the Month at 9 AM | CronBase
0 9 L * * At nine o'clock in the morning on the last day of every month
The `0 9 L * *` cron expression schedules a task to run precisely at nine AM on the final day of every month, regardless of whether the month has twenty-eight, thirty, or thirty-one days. It is commonly used for generating monthly financial statements, processing end-of-month billing cycles, and compiling aggregate usage reports.
- Minute
- 0
- Hour
- 9
- Day of Month
- L
- Month
- *
- Day of Week
- *
* Tools
Code & Implementations
#!/usr/bin/env bash
set -euo pipefail
# This script is designed to run daily at 9:00 AM via standard cron: 0 9 28-31 * *
# It acts as a safety wrapper for environments where 'L' is not natively supported.
is_last_day_of_month() {
# Check if tomorrow's day of the month is 01
[[ "$(date -d "tomorrow" +%d)" -eq "01" ]]
}
if ! is_last_day_of_month; then
echo "Today is not the last day of the month. Exiting gracefully."
exit 0
fi
echo "Starting end-of-month execution at $(date)"
/usr/local/bin/run-monthly-reports.sh › Setup notes
Save this script to /usr/local/bin/monthly-wrapper.sh, make it executable with 'chmod +x', and schedule it in crontab using '0 9 28-31 * * /usr/local/bin/monthly-wrapper.sh'.
const cron = require('node-cron');
const { exec } = require('child_process');
// Since node-cron doesn't natively support 'L', we run a check daily at 9:00 AM
cron.schedule('0 9 28-31 * *', () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
// If tomorrow's date is 1, then today is the last day of the month
if (tomorrow.getDate() === 1) {
console.log('Executing monthly report job...');
runMonthlyJob();
} else {
console.log('Skipping job: Not the last day of the month.');
}
}, {
scheduled: true,
timezone: "UTC"
});
function runMonthlyJob() {
exec('/usr/local/bin/monthly-task.sh', (error, stdout, stderr) => {
if (error) {
console.error(`Job failed: ${error.message}`);
return;
}
console.log(`Job output: ${stdout}`);
});
} › Setup notes
Install 'node-cron' using npm, then run this script as a background daemon process using PM2 or systemd to ensure continuous execution.
import datetime
import sys
import subprocess
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
def is_last_day_of_month():
today = datetime.date.today()
tomorrow = today + datetime.timedelta(days=1)
return tomorrow.day == 1
@scheduler.scheduled_job('cron', hour=9, minute=0, day='28-31', timezone='UTC')
def monthly_job_trigger():
if not is_last_day_of_month():
print("Skipping execution: Today is not the last day of the month.")
return
print(f"Starting scheduled monthly task at {datetime.datetime.utcnow()}")
try:
result = subprocess.run(["/usr/local/bin/monthly-billing.sh"], capture_output=True, text=True, check=True)
print(f"Task completed successfully: {result.stdout}")
except subprocess.CalledProcessError as err:
print(f"Task execution failed: {err.stderr}", file=sys.stderr)
if __name__ == "__main__":
print("Starting scheduler daemon...")
scheduler.start() › Setup notes
Install 'apscheduler' via pip, configure the target script path, and run this script as a persistent service inside your application stack.
package main
import (
"context"
"log"
"os/exec"
"time"
"github.com/robfig/cron/v3"
)
func isLastDayOfMonth(t time.Time) bool {
tomorrow := t.AddDate(0, 0, 1)
return tomorrow.Day() == 1
}
func main() {
nyc, err := time.LoadLocation("UTC")
if err != nil {
log.Fatalf("Failed to load timezone: %v", err)
}
c := cron.New(cron.WithLocation(nyc))
_, err = c.AddFunc("0 9 28-31 * *", func() {
now := time.Now().In(nyc)
if !isLastDayOfMonth(now) {
log.Println("Not the last day of the month. Skipping run.")
return
}
log.Println("Executing monthly data aggregation pipeline...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, "/usr/local/bin/aggregate-data.sh")
if err := cmd.Run(); err != nil {
log.Printf("Pipeline execution failed: %v", err)
}
})
if err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
c.Start()
select {}
} › Setup notes
Run 'go get github.com/robfig/cron/v3' to fetch the library, compile the binary, and run it as a systemd service in your cluster.
package com.cronbase.scheduler;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
@Component
public class MonthlyTaskScheduler {
private static final Logger LOGGER = Logger.getLogger(MonthlyTaskScheduler.class.getName());
// Spring supports the 'L' character natively.
// Spring cron uses 6 fields: second, minute, hour, day-of-month, month, day-of-week.
@Scheduled(cron = "0 0 9 L * ?", zone = "UTC")
public void executeMonthlyJob() {
LOGGER.info("Starting monthly financial close task at: " + LocalDateTime.now());
try {
processFinancialRecords();
LOGGER.info("Monthly financial close completed successfully.");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Critical failure during monthly execution", e);
}
}
private void processFinancialRecords() throws Exception {
Thread.sleep(5000); // Simulated workload
}
} › Setup notes
Ensure '@EnableScheduling' is declared in your Spring Boot application configuration. The system will handle the 'L' wildcard automatically.
apiVersion: batch/v1
kind: CronJob
metadata:
name: monthly-report-job
namespace: production
spec:
# Kubernetes standard cron does not support 'L'.
# We schedule it to run daily between the 28th and 31st at 09:00 UTC.
schedule: "0 9 28-31 * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
containers:
- name: report-worker
image: busybox:1.36
command:
- /bin/sh
- -c
- |
set -e
TOMORROW=$(date -d "tomorrow" +%d)
if [ "$TOMORROW" != "01" ]; then
echo "Today is not the last day of the month. Exiting gracefully."
exit 0
fi
echo "Last day of the month confirmed. Executing pipeline..."
sleep 10
restartPolicy: OnFailure › Setup notes
Apply this manifest using 'kubectl apply -f cronjob.yaml'. The inline wrapper script dynamically ensures execution only occurs on the final day.
Last verified:
Monitor this schedule in production
Get alerted the moment this cron job fails, is late, or doesn't run. BetterStack tracks execution, duration, and output — no infrastructure required.
Platform Equivalents
Systemd Timer
OnCalendar*-*-* 09:00:00
[Unit]
Description=Timer for cron expression: 0 9 L * *
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target
Last verified:
Frequently Asked Questions
What happens if my cron engine does not support the 'L' character?
If your cron engine (like standard Linux crontab) does not support 'L', you must schedule the job to run daily from the 28th to the 31st using '0 9 28-31 * *' and prepend a date validation command to ensure the script only executes if tomorrow is the 1st.
How does Daylight Saving Time (DST) affect a 9:00 AM monthly execution?
If your server runs on local time zones that observe DST, the job will shift relative to UTC. To guarantee consistent 24-hour intervals and prevent reporting anomalies, run your server and cron daemon in Coordinated Universal Time (UTC).
Is '0 9 L * *' supported natively in Kubernetes CronJobs?
No, Kubernetes uses the Go-based robfig/cron parser, which does not support the 'L' wildcard. You must implement a workaround, such as scheduling the job daily between the 28th and 31st and evaluating the date inside your container entrypoint.
How can I prevent database connection pool exhaustion during this end-of-month run?
Because monthly jobs process high volumes of data, isolate the database pool for this job. Implement strict timeouts, limit concurrent queries, and use cursor-based batching instead of loading all monthly transaction records into memory at once.
How do I schedule this in Quartz or Spring Scheduler?
In Quartz and Spring, you must use a 6-field cron syntax and replace the day-of-week field with a question mark to avoid conflicts. The correct expression is '0 0 9 L * ?' to run at exactly 9:00:00 AM on the last day of every month.
* Explore
Related expressions you might need
Last verified: