Cron syntax, explained properly

Everything the man page assumes you already know: the five fields, the special characters, the timezone traps, and the failure modes that only show up in production. Ten minutes here saves you the 2am incident later.

The five fields

┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12 or JAN–DEC)
│ │ │ │ ┌───── day of week (0–6, 0 = Sunday)
│ │ │ │ │
* * * * *  command-to-run

Each field accepts a value, a range, a list, or a step — and * means “every”. Reading an expression is just filling in the sentence: “run at minute M of hour H on day D…”. A few worked examples:

Minute and hour

The workhorses. 0–59 and 0–23, and this is where steps live: */15 in the minute field is every quarter hour, */2 in the hour field is every other hour. One non-obvious detail: steps count from the start of the field’s range, not from when you installed the job. */5 fires at :00, :05, :10 — never at :03, :08, :13.

Day of month and month

1–31 and 1–12 (month names JAN–DEC work in most implementations). The trap: days 29–31 don’t exist in every month. A job on day 31 runs seven times a year; day 30 skips February; and standard cron has no “last day of month” — see the workaround on the first-of-month page.

Day of week

0–6 with 0 = Sunday (names SUN–SAT also work). Vixie cron additionally accepts 7 for Sunday; POSIX and BusyBox don’t — write 0 and it works everywhere. And if you ever port an expression to Quartz (Java), beware: there 1 = Sunday, so Linux 1-5 (Mon–Fri) becomes Sun–Thu. That single off-by-one has shipped a lot of Saturday emails.

Special characters, from everywhere to exotic

SyntaxMeaningWorks in
*every valueeverywhere
,list — 1,4,7,10 = quarterlyeverywhere
-range — 1-5 = Mon–Frieverywhere
/step — */10 = every 10everywhere
@daily, @hourly, @rebootshorthandsVixie/cronie, not POSIX
L, W, #last day, nearest weekday, “2nd Tuesday”Quartz & some clouds only
?“no specific value” for day fieldsQuartz only

Rule of thumb: stick to * , - / and your expression is portable across crontab, GitHub Actions, Kubernetes, and almost everything else. The moment you use L or ?, you’ve married one scheduler.

The OR quirk nobody expects

When both day-of-month and day-of-week are restricted, classic cron runs the job when either matches — not both. 0 0 13 * 5 doesn’t mean “Friday the 13th”; it means “every 13th and every Friday”. To get the intersection, restrict one field in the expression and check the other in the command: 0 0 13 * * [ "$(date +\%u)" = 5 ] && run-spooky-job.

Timezones and DST: where cron jobs go to die

An expression has no timezone of its own — it fires in the scheduler’s timezone, and every platform picks a different one:

Daylight saving adds the sharp edge: in a TZ-aware scheduler, a job at 02:30 local can run twice on the fall-back night and never on the spring-forward night. Two defenses: schedule money-touching jobs in UTC, or keep wall-clock times outside 01:00–03:00. (Full background: crontab(5).)

The three production failure modes

1. Overlap

Cron starts your job on schedule whether or not the previous run finished. A every-minute job that occasionally takes 90 seconds will stack copies until the box falls over. Fix: flock turns overlap into a skipped run — * * * * * flock -n /tmp/job.lock your-command.

2. The thundering herd

Everyone schedules at :00, midnight most of all. Your DB, your API providers, and every other tenant on the host spike together. If the exact minute doesn’t matter, don’t use minute 0 — 17 * * * * is the same hourly job without the queueing, and 0 3 * * * beats midnight for nightly work.

3. Silent death

Cron has no retries, no alerting, and emails its errors to a local mailbox nobody reads. A weekly job that fails waits seven days for its next chance — and a yearly one fails years after its author left. The fix is a dead-man’s-switch monitor: the job pings a URL on success, and you get alerted when the ping doesn’t arrive.

Will you know if this job silently fails?

Cron jobs fail quietly — a server reboots, a path changes, or an error code is ignored — and nobody notices until the data is missing. A cron monitor (a dead-man’s-switch) alerts you when a scheduled job does not check in on time.

Monitor your cron jobs with UptimeRobot →

Disclosure: this is an affiliate link — we may earn a commission if you sign up, at no extra cost to you.

Grab a ready-made expression

Or skip the theory — use the interactive generator & explainer, or copy one of these: