apprun.icuGuides

Guide · Developer tools

Cron expressions explained field by field

A cron entry is five fields and one matching minute. Everything that goes wrong with a schedule comes from reading one of those fields the way you meant it rather than the way cron does.

By apprun.icuUpdated Read 7 minIncludes a working browser tool
On this page
  1. The short answer
  2. The five fields
  3. Characters that are not standard cron
  4. The @ shortcuts
  5. Day of month versus day of week: it is OR
  6. How a job is actually scheduled
  7. Cron or a systemd timer
  8. Check the schedule before it ships
  9. Common questions
  10. Sources and further reading
Five fields, one matching minute, one commandm h dom mon dowFive fields,left to rightAll five matchthis one minutecron tests everyminute, 0–59Command runsin local timeand it starts a newrun every minute
cron has no next-run function: it wakes every minute, reads the local clock and starts the command when the five fields match.

The five fields

The order is fixed and the ranges are small: minute 0-59, hour 0-23, day of month 1-31, month 1-12, day of week 0-7, where 0 and 7 both mean Sunday. Names work in the month and day-of-week fields, case does not matter, inside ranges and lists too. There is no seconds field, which is what people miss arriving from Quartz. A sixth year field exists on Debian's cron and on cronie; it is not POSIX, so the same file can behave differently across implementations.

Every field expands: * covers the range, 1-5 is an inclusive range, 0,30 a list, */15 a step, 0-10/5 is 0, 5, 10. A step restarts at the bottom of the range, so */15 is 0, 15, 30 and 45 — anchored to the hour, never counted forward from the last run. The manual page is blunt about why: */35 does not run every 35 minutes, it runs twice an hour, at 0 and 35. */0 never advances and matches nothing.

Characters that are not standard cron

L, W, # and ? come from Quartz, and a plain crontab rejects all four. They matter because application configuration full of them eventually meets a real crontab.

L means "last": in the day-of-month field the last day of the month, with L-3 for three days before it; in the day-of-week field the last matching weekday, so 6L is the last Friday. W picks the nearest weekday to a date and never crosses a month boundary, so 15W fires on Friday the 14th when the 15th is a Saturday; LW is the last weekday. # selects an occurrence: 6#3 is the third Friday, and 4#5 does not fire in a month without a fifth Wednesday. ? means "no specific value", marking the day field that is not deciding.

The @ shortcuts

Six of the seven nicknames are the entries they stand for: @yearly and @annually are 0 0 1 1 *, @monthly is 0 0 1 * *, @weekly is 0 0 * * 0 for Sunday, @daily and @midnight are 0 0 * * *, and @hourly is 0 * * * *. @midnight is not in every cron manual page.

@reboot is the odd one out and the only shortcut that is not a schedule. It runs the command once when the cron daemon itself starts — usually at boot — and never again on a timer. It has no next run time and waits for no dependency. Its real use is starting a service the machine needs after boot; ordering is a systemd unit's job.

Day of month versus day of week: it is OR

This is the most misunderstood part of cron, and it is deliberate rather than a bug. From the Vixie crontab(5) page: if both day fields are restricted, that is, neither contains the * character, the command runs when either field matches. Its own example is 30 4 1,15 * 5: 04:30 on the 1st and 15th of every month, plus every Friday — roughly eight runs a month, not two.

The fix is usually to delete one of the two fields: 30 4 1,15 * * is the 1st and 15th only, 30 4 * * 1-5 is weekdays only. "The first Monday of the month" is not expressible in Vixie cron at all, which is why 30 4 1 * 1 fires on the 1st plus every Monday.

How a job is actually scheduled

cron does not compute a next run. The daemon sleeps until the next minute, reads the local clock, tests the fields and forks the command if they match. Three consequences follow.

Everything is the server's local time. An entry carries no timezone, so one crontab means different minutes in two offices. The documented escape hatch is a CRON_TZ= line, a cronie and Debian cron feature rather than something to rely on everywhere. Related background: Unix timestamps, seconds and milliseconds.

Daylight saving moves the target. The manual page is explicit: non-existent times never match, so a job inside the skipped hour does not run at all, and times that occur twice cause matching jobs to run twice. A 02:30 job is skipped on a spring-forward night and runs twice on a fall-back night in a zone that shifts at 02:00. Schedule in UTC when a job must run exactly once.

Try it with your own input

Your result

The workbench below converts a timestamp into your own zone, the fastest way to check what a stored fire time means on the machine that will run it.

Nothing waits for anything. cron starts a new run every minute whether or not the last one finished, so a slow job quietly overlaps itself, and the environment is minimal — SHELL, LOGNAME and a restricted PATH — which is why full paths in commands save an afternoon. One parsing trap gives baffling output rather than an error: cron reads the command only up to the first unescaped % and sends the rest as standard input, so date +%Y must be written date +\%Y.

Cron or a systemd timer

On a systemd host a timer is usually the better tool. OnCalendar= takes a calendar expression close to cron's, so the five-field habit transfers directly. OnUnitActiveSec= takes a real interval measured from the last activation, which cron cannot express: it cannot drift, double across a clock change or skip an hour. Persistent=true runs the job at the next boot if it was missed.

Calendar timers inherit cron's daylight-saving behaviour, because they are still wall-clock events; interval timers are the immune ones. The question is therefore not "systemd or cron" but whether the job means a time of day or a gap between runs. cron remains the shortest path on a minimal VPS image, on macOS and the BSDs.

Check the schedule before it ships

Looking at the list of times a schedule produces, rather than the expression you believed you wrote, catches nearly everything. For 0 9 * * 1-5, check that the first entry lands on a Monday. Count the runs: a weekday schedule is five a week, and a count that is a multiple of seven means a field you thought was restricted is not. Look for impossible dates, such as 30 2 30 2 *, which asks for 02:30 on 30 February — any tool searching for the next match needs a cap, or it searches forever. Check the boundaries too: 23:59 on the last day of the month, and any 00:0x job on the 1st, are the two most often reasoned about in the wrong timezone.

On a real machine, do not rely on a browser. systemd-analyze calendar '0 9 * * 1-5' prints the next elapses for a systemd spec, Debian's cron validates a file without installing it with crontab -T, and the daemon's log is the only source of truth.

Common questions

What does */15 mean in a cron expression?

*/15 in the minute field means minutes 0, 15, 30 and 45: four runs an hour, anchored to the top of the hour. A step always restarts at the bottom of the field's range, so it never counts forward from the previous run. The crontab(5) page makes the consequence plain: */35 does not run every 35 minutes, it runs twice an hour, at 0 and 35.

If both day fields are set, is it AND or OR?

OR. When both day fields are restricted, meaning neither is a bare *, the command runs when either one matches. So 30 4 1,15 * 5 runs at 04:30 on the 1st and 15th of every month and on every Friday. If one of them is *, only the other applies.

What does @reboot do?

It runs the command once when the cron daemon itself starts, usually at boot but also on a daemon restart, and never repeats it on a timer. It has no next run time and waits for no dependency to come up. Right hook for starting a service after boot, wrong one for anything that must repeat.

How does cron handle time zones and daylight saving?

Cron evaluates every schedule in the server's local time, because an entry carries no timezone of its own. crontab(5) states that non-existent times in a daylight saving transition never match, so a job inside the skipped hour does not run, and times that occur twice cause matching jobs to run twice. Schedule in UTC when a job must run exactly once.

Are L, W, # and ? valid in a crontab?

No, they are Quartz extensions and a Vixie crontab rejects them. L is the last day of the month or the last matching weekday, W the weekday nearest a date without crossing a month boundary, # an occurrence such as the third Friday, and ? "no specific value" in the day field that is not deciding. Quartz also numbers weekdays 1-7 from Sunday while cron uses 0-7, so the same digits mean different days.

Sources and further reading

Share this guide