Run Monthly Jobs on the 28th at Midnight | CronBase
0 0 28 * * At midnight on the twenty-eighth day of every month
The `0 0 28 * *` cron schedule executes a task exactly at midnight on the twenty-eighth day of every single month. This specific timing is highly reliable for monthly recurring billing, financial ledger reconciliation, and system report generation, as the twenty-eighth is guaranteed to exist in every month of the calendar year, including February.
- Minute
- 0
- Hour
- 0
- Day of Month
- 28
- Month
- *
- Day of Week
- *
Next 5 Runs
- in 2d 3h
- in 33d 3h
- in 64d 3h
- in 94d 3h
- in 125d 3h
* Tools
Code & Implementations
#!/usr/bin/env bash
# Production Bash script designed to run monthly on the 28th.
# Uses a lock file to prevent concurrent executions.
set -euo pipefail
LOCKFILE="/var/lock/monthly_ledger_run.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Error: Another instance is running." >&2; exit 1; }
echo "[$(date -u)] Starting monthly financial ledger processing..."
# Call downstream service or run database cleanup
if ! curl -f -s -X POST https://api.internal/v1/ledger/close; then
echo "Error: Ledger closure API call failed." >&2
exit 1
fi
echo "[$(date -u)] Monthly task completed successfully." › Setup notes
Save the script to /usr/local/bin/monthly-ledger.sh, run chmod +x, and add 0 0 28 * * /usr/local/bin/monthly-ledger.sh to the system crontab.
const cron = require('node-cron');
const logger = require('./logger'); // Production logger abstraction
// Schedule task for 0 0 28 * * (Midnight on the 28th of every month)
cron.schedule('0 0 28 * *', async () => {
logger.info('Starting monthly data archiving task...');
try {
await performMonthlyArchive();
logger.info('Monthly data archiving completed successfully.');
} catch (error) {
logger.error('Failed to complete monthly archiving:', error);
// Integrate notification hooks (e.g., Sentry, Slack, PagerDuty) here
}
}, {
scheduled: true,
timezone: "UTC"
});
async function performMonthlyArchive() {
// Simulate archive processing
return new Promise((resolve) => setTimeout(resolve, 5000));
} › Setup notes
Install the required package with npm install node-cron and run this script as a daemon process using PM2 or a similar process manager.
import logging
import sys
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('monthly_scheduler')
def run_monthly_billing():
logger.info("Initiating monthly subscription billing run...")
try:
# Simulate billing processing
logger.info("Monthly subscription billing completed successfully.")
except Exception as e:
logger.error(f"Critical error during monthly billing: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
scheduler = BlockingScheduler()
# 0 0 28 * * -> minute=0, hour=0, day=28, month=*, day_of_week=*
trigger = CronTrigger(minute=0, hour=0, day=28, timezone='UTC')
scheduler.add_job(run_monthly_billing, trigger)
logger.info("Scheduler initialized. Monthly job registered for the 28th at midnight UTC.")
try:
scheduler.start()
except (KeyboardInterrupt, SystemExit):
logger.info("Scheduler stopped.") › Setup notes
Install dependencies using pip install apscheduler and execute the script. Ensure system time is configured to UTC.
package main
import (
"context"
"log"
"time"
"github.com/robfig/cron/v3"
)
func main() {
// Explicitly use UTC to avoid local system timezone discrepancies
loc, err := time.LoadLocation("UTC")
if err != nil {
log.Fatalf("Failed to load UTC location: %v", err)
}
c := cron.New(cron.WithLocation(loc))
// Schedule the standard cron spec: 0 0 28 * *
_, err = c.AddFunc("0 0 28 * *", func() {
log.Println("Starting monthly backup snapshot generation...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()
if err := executeMonthlyBackup(ctx); err != nil {
log.Printf("CRITICAL: Monthly backup failed: %v", err)
} else {
log.Println("Monthly backup snapshot completed successfully.")
}
})
if err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
c.Start()
log.Println("Cron runner started. Job scheduled for the 28th of every month at midnight.")
select {} // Keep application running
}
func executeMonthlyBackup(ctx context.Context) error {
// Simulate backup logic with timeout safety
time.Sleep(2 * time.Second)
return nil
} › Setup notes
Add github.com/robfig/cron/v3 to your project using go get and run the executable as a background daemon.
package com.example.scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MonthlyReportScheduler {
private static final Logger logger = LoggerFactory.getLogger(MonthlyReportScheduler.class);
// Standard cron: 0 0 28 * * (Spring uses 6 fields: sec min hour day month day-of-week)
@Scheduled(cron = "0 0 0 28 * *", zone = "UTC")
public void executeMonthlyReport() {
logger.info("Scheduled execution started: Monthly performance report generation.");
try {
generateReports();
logger.info("Monthly performance report generation completed successfully.");
} catch (Exception e) {
logger.error("CRITICAL: Failed to generate monthly reports: ", e);
// Trigger alerting infrastructure here
}
}
private void generateReports() throws InterruptedException {
// Simulate reporting logic
Thread.sleep(5000);
}
} › Setup notes
Ensure @EnableScheduling is declared in your Spring Boot application configuration. Place this component inside a scanned package.
apiVersion: batch/v1
kind: CronJob
metadata:
name: monthly-data-reconciliation
namespace: production
spec:
schedule: "0 0 28 * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
template:
spec:
containers:
- name: reconciler
image: internal-registry.corp/data-ops/reconciler:v1.4.2
imagePullPolicy: IfNotPresent
env:
- name: ENVIRONMENT
value: "production"
resources:
limits:
cpu: "1000m"
memory: "2Gi"
requests:
cpu: "500m"
memory: "1Gi"
restartPolicy: OnFailure › Setup notes
Apply this configuration to your Kubernetes cluster using kubectl apply -f cronjob.yaml and verify the schedule with kubectl get cronjob.
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
AWS EventBridge
Standard cron expressions often need conversion for AWS EventBridge schedules.
cron(0 0 28 * ? *) Systemd Timer
OnCalendar*-*-28 00:00:00
[Unit]
Description=Timer for cron expression: 0 0 28 * *
[Timer]
OnCalendar=*-*-28 00:00:00
Persistent=true
[Install]
WantedBy=timers.target
Last verified:
Frequently Asked Questions
Why is the 28th chosen instead of the 30th or 31st for monthly jobs?
The 28th is the highest day number that exists in every single month of the year, including February. Choosing the 30th or 31st would require complex custom logic to handle shorter months, making the 28th the safest choice for consistent monthly intervals.
How does Daylight Saving Time (DST) affect a midnight cron job?
If your server runs on local time, DST transitions can cause the midnight job to run twice or be skipped entirely. To prevent this, configure your cron daemon or application runtime to execute in Coordinated Universal Time (UTC).
What happens if the monthly job fails to run due to server downtime?
Standard cron has no built-in catch-up mechanism. You should use a tool like anacron, implement a state-based database flag to track execution, or use a scheduling framework with 'misfire' instructions to run missed executions upon recovery.
How can I prevent database connection timeouts during this heavy monthly run?
Introduce a random startup delay (jitter) of several minutes before the main execution logic begins. This staggers the initial database connections and prevents resource starvation, especially when multiple microservices trigger concurrent monthly tasks.
Can I restrict this monthly job to run only on weekdays?
Standard cron does not easily support '28th of the month AND only weekdays' in a single expression because day-of-month and day-of-week fields act as an OR relationship. You must handle this filter programmatically within your script or application code.
* Explore
Related expressions you might need
Last verified: