Cron Expressions for Monthly and Yearly Schedules
Billing runs, monthly reports, and yearly cleanups all need schedules that fire on specific dates rather than fixed intervals. Cron handles most of these cleanly with the day-of-month and month fields — except for one common request it can't express natively: "the last day of the month." Here's how to handle both cases correctly.
Monthly schedules — first of the month and specific dates
Set the day-of-month field (3rd field) to the date you want, and leave month and day-of-week as wildcards so it repeats every month:
0 0 1 * * → midnight on the 1st of every month 0 9 1 * * → 9:00 AM on the 1st of every month 0 0 15 * * → midnight on the 15th of every month 0 6 1,15 * * → 6:00 AM on the 1st AND 15th of every month 0 0 5 * * → midnight on the 5th of every month (e.g. after month-end reconciliation)
Why "last day of month" doesn't exist in standard cron
Standard cron fields are static ranges — day-of-month accepts 1 through 31. There is no value that means "whatever the last valid day happens to be this month," because that number changes (28, 29, 30, or 31) depending on the month and leap years. Two reliable workarounds exist:
- Run on the 1st of the next month instead. If the job is a month-end report, running it at 00:05 on the 1st and having it process "yesterday" is usually equivalent and far simpler.
- Run every day near month-end and check inside the script. Schedule the job for the 28th–31st daily, then have the script itself compute whether "tomorrow" rolls into a new month, and only proceed if so.
# Runs daily at 23:55 from the 28th through the 31st, # script itself decides if today is actually the last day 55 23 28-31 * * /opt/scripts/month-end-check.sh # month-end-check.sh (bash) #!/bin/bash TOMORROW=$(date -d "+1 day" +\%d) if [ "$TOMORROW" -eq "01" ]; then /opt/scripts/run-month-end-report.sh fi
Node.js's node-cron and Quartz support this more elegantly — Quartz has a native L ("last") value: 0 0 0 L * ? means "midnight on the last day of every month," no manual date math needed.
Quarterly schedules
List the specific months in the month field (4th field) instead of using a wildcard:
0 0 1 1,4,7,10 * → midnight on the 1st of Jan, Apr, Jul, Oct (quarterly) 0 9 1 3,6,9,12 * → 9 AM on the 1st of Mar, Jun, Sep, Dec
Yearly (annual) schedules
Fix both the day-of-month and month fields to a specific date. Standard cron has no year field, so the job simply repeats on that date every year automatically:
0 0 1 1 * → midnight on January 1st every year (annual reset job) 0 0 1 4 * → midnight on April 1st every year (India fiscal year start) 0 9 25 12 * → 9:00 AM on December 25th every year 0 0 31 3 * → midnight on March 31st every year (India fiscal year end)
For jobs that should only run in a specific year (e.g. a one-time migration), don't rely on cron's date fields at all — schedule it as a one-off with at instead, or add an explicit year check inside the script and remove the crontab entry once it has run.
Frequently Asked Questions
The cron expression 0 0 1 * * runs at midnight on the 1st day of every month. The day-of-month field is set to 1, and month and day-of-week are left as wildcards.
No. Standard 5-field Unix cron has no native concept of "last day of month" because months have different lengths. The common workaround is to schedule the job on the 1st of the next month, or check the date inside the script itself.
Set the month and day-of-month fields to a fixed value and leave everything else as usual, for example 0 0 1 1 * runs once a year at midnight on January 1st. Standard cron has no year field, so this repeats every year automatically.