Cron on the last day of the month: the L workaround
There is no last day of month syntax here. The L special character is rejected, so the standard approach is to schedule the job on days 28 to 31 and have the script exit unless tomorrow is the 1st.
At a glance
- Expression
- 0 0 28-31 * * with a tomorrow is the 1st guard
- Fires
- 12 times a year of real work, from 12 to 16 scheduled runs a year depending on how many of days 28 to 31 exist.
- Restricted fields
- Day of month restricted to a range, minute and hour pinned. The real restriction lives in the script.
- Equivalent macro
- None. No macro refers to the end of a month.
- Why L does not work
- The parser rejects L, W, # and ? with a named error. They are Quartz extensions, not standard cron syntax.
- Scheduled versus useful runs
- Four scheduled runs in a 31 day month, one of which does the work.
In detail
The end of the month is a moving target: the 28th, 29th, 30th or 31st depending on which month it is. Standard cron has no field that can express it, and the Quartz style L character that other schedulers use for exactly this purpose is rejected here with an explicit error rather than being silently ignored.
The reliable workaround puts the logic where the logic can live. Schedule the job on days 28 through 31, which covers every possible last day, and have the script check whether tomorrow is the first of a month. If it is not, the script exits immediately, which is a fraction of a second of wasted work three days a month at most.
The test itself is a one liner with the date command. Asking for the day number of tomorrow and comparing it to 01 is unambiguous and does not require knowing the month length or the leap year rule. On systems with GNU coreutils the syntax is date -d tomorrow, and elsewhere it is date -v+1d, which is worth checking before deploying to an unfamiliar image.
One thing to watch is the percent sign. A date format string is full of them, and cron treats an unescaped percent in a command as the end of the command, feeding everything after it to the program on standard input. Every percent in a crontab line has to be written as a backslash followed by a percent.
Examples
0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /opt/app/bin/month-close.shRuns at 23:00 on days 28 to 31, and does the work only on the actual last day.
0 23 28-31 * * /opt/app/bin/if-last-day.sh /opt/app/bin/month-close.sh >> /var/log/close.log 2>&1Keeps the percent escaping out of the crontab entirely, which is usually worth the extra file.
0 0 L * *The parser reports that L, W, # and ? are not supported in the day of month field.
Things that catch people out
- Scheduling only on the 31st does not give you the last day. It gives you seven runs a year and no run in February.
- date -d tomorrow is a GNU extension. On a BSD userland the equivalent is date -v+1d, and getting this wrong means the guard never passes.
- Every percent sign in a crontab command needs a backslash in front of it, including the ones inside a date format string.
Try it: Cron Expression Parser
Paste any five or six field expression to get a plain English description, a per field breakdown and the next run times in your local time.
Cron Expression Parser
LOCAL · Browser-OnlyParse cron expressions and see the next scheduled run times
Supports 5-field (minute hour day month weekday), 6-field (with seconds), named values (JAN, MON), and @macros (@daily, @hourly, ...)
Use the dropdowns below to build your cron expression visually, or type a custom value in each field.
Human-Readable Description
Every 5 minutes
Field Breakdown
| Field | Allowed | Value | Meaning |
|---|---|---|---|
| Minute | 0-59 | */5 | every 5 minute(s) |
| Hour | 0-23 | * | every hour |
| Day of Month | 1-31 | * | every day of month |
| Month | 1-12 | * | every month |
| Day of Week | 0-7 | * | every day of week |
Related cron expressions
All cron expressions →Cron on the 31st: 0 0 31 * * only fires seven times a year
0 0 31 * * runs only in months that have 31 days, so it fires seven times a year, not twelve. The same trap hits day 30 and day 29.
Cron L, W, # and ?: the Quartz characters this parser rejects
The Quartz extensions L, W, # and ? are rejected here with a named error. What each one means in Quartz, and what to write in standard cron instead.
Cron on the first day of the month: 0 0 1 * *
0 0 1 * * runs at midnight on the 1st of every month, twelve times a year, and is exactly what the @monthly macro expands to.
Cron on the last Friday of the month: no # or L support
The Quartz forms 6L and 6#5 are rejected here. Schedule every Friday and let the script check whether the following Friday is in the next month.
Cron on the first Monday of the month: the OR rule trap
0 9 1-7 * 1 does not mean the first Monday. Because both date fields are restricted, it fires on days 1 to 7 and on every Monday as well.
Cron every 2 months: 0 0 1 */2 * and the odd month set
0 0 1 */2 * fires on the 1st of January, March, May, July, September and November. Use 0 0 1 2/2 * for the even months instead.
Tools for this
- Cron Expression Parser Free online cron expression parser - parse cron expressions and see the next scheduled run times
- Crontab Generator Free online crontab generator - build cron expressions visually with an intuitive interface