Cron syntax — every field, every quirk
On this page
Cron is 50 years old. It has five fields. It still trips up intermediate engineers regularly. This is the comprehensive walkthrough — every field with what’s allowed, plus the three traps that don’t appear in most documentation.
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 or SUN–SAT, both 0 and 7 = Sun)
│ │ │ │ │
* * * * *
Five space-separated values, each describing one component of the schedule. The job fires when all five fields match the current time.
(With one massive exception, covered below.)
What each field accepts
Every field accepts:
- Single value:
5,MON,0 - Wildcard:
*(matches any value) - List:
1,3,5(matches any of these) - Range:
1-5(matches any value in this range, inclusive) - Step:
*/15(every Nth value, starting from min) - Range with step:
0-30/5(every 5th value within range)
Combinations are allowed: 0,15,30,45 and */15 are equivalent.
1-10/2,15 matches 1, 3, 5, 7, 9, 15.
The minute and hour fields are pure integers. The month and day-of-week fields additionally accept three-letter names — case insensitive.
Field-by-field
Minute (0–59)
The most-used field for */N steps. */5 = every 5 minutes. */30 =
every half hour. 0,15,30,45 = quarter hours.
Note: cron operates at minute granularity. There’s no built-in “every 30 seconds” — you’d need a different scheduler (systemd timers support sub-minute, Quartz can do seconds, plain crontab can’t).
Hour (0–23)
24-hour. 0 is midnight, 23 is 11 PM. No 12 AM / 12 PM.
8-17 covers a typical work day (8am–5pm). 9-17/2 runs every 2
hours within that range (9, 11, 13, 15, 17).
Day of month (1–31)
1 is the first of the month. 15 is the middle. There’s no last day of month in standard cron — Quartz has L for this; standard
cron requires a workaround like @daily + a runtime check.
*/2 (every other day) is generally a bad idea — it skips inconsistently
across month boundaries. 1,15 is more predictable.
Month (1–12 or JAN–DEC)
1 is January. Names are case-insensitive: JAN, Jan, jan all work.
6-8 is summer (Jun–Aug). */3 is quarterly (Jan, Apr, Jul, Oct).
Day of week (0–6 or SUN–SAT)
0 is Sunday in most cron implementations, including Vixie/POSIX. 7
also means Sunday in some implementations (Vixie, BSD) but not all.
For portability, use 0 for Sunday.
Names: SUN, MON, TUE, WED, THU, FRI, SAT.
1-5 = weekdays. 6,0 = weekends. MON,WED,FRI = MWF.
The three traps
Trap 1: day-of-month OR day-of-week
This is the big one. If both day-of-month and day-of-week are
restricted (i.e., not *), Vixie cron fires when EITHER matches. Not
their intersection.
So 0 9 1 * 1 doesn’t mean “9am on the 1st of any month, but only if
it’s a Monday.” It means “9am on the 1st of any month OR any
Monday.” Across a year that’s roughly 12 + 52 = 64 fires, not the
maybe-1-or-2 you’d expect.
This is intentional, dating back to AT&T’s original cron, but unintuitive. The reasoning: the field semantics work as logical-OR because they describe two different selection axes.
Fix: never restrict both. Pick one:
- For “9am on the 15th of every month”:
0 9 15 * *(DoW =*) - For “9am every Monday”:
0 9 * * 1(DoM =*) - For “9am on the 15th, but only if Monday”: handle inside the job:
scheduled with[ "$(date +%u)" = "1" ] && /run/job0 9 15 * *.
The parser on this site shows the next-fire times directly so you can verify your intent.
Trap 2: missed runs and DST
Cron uses local time. Two days a year (in DST-observing timezones), the calendar skips an hour or repeats one.
- Spring forward: 02:30 doesn’t exist. Jobs scheduled at 02:30 do not run that day on most cron implementations.
- Fall back: 02:30 happens twice. Some cron implementations run the job twice; some run it once; some skip it. Behaviour differs across cron, anacron, systemd timers.
Fix: schedule jobs at 03:00+ to avoid the DST hour. For UTC-based infrastructure, set the cron’s TZ to UTC explicitly:
TZ=UTC
0 5 * * * /run/job
(See date.tooljo.com/blog/dst-and-date-math for the full DST gotcha taxonomy.)
Trap 3: long-running jobs and overlap
If a cron job takes longer to run than the interval between fires
(say, you scheduled */5 * * * * and it takes 10 minutes), cron starts
a second instance while the first is still running. Most jobs don’t
handle this gracefully:
- Two competing writes to the same file
- Two database transactions racing for the same lock
- Resource exhaustion on the host
Fix: wrap the job in flock (Linux) or use a job system that handles
overlap explicitly:
*/5 * * * * flock -n /tmp/myjob.lock /run/job
flock -n exits immediately if the lock is held. The next fire either
runs (lock free) or skips (still locked).
Special strings
Most cron implementations support these synonyms:
| String | Equivalent |
|---|---|
@yearly / @annually | 0 0 1 1 * |
@monthly | 0 0 1 * * |
@weekly | 0 0 * * 0 |
@daily / @midnight | 0 0 * * * |
@hourly | 0 * * * * |
@reboot | At system boot (not on a schedule) |
@reboot is the odd one — it isn’t a time at all; it tells cron to run
the job once at startup. Useful for replacing legacy init.d scripts.
Quartz extensions (don’t apply to standard cron)
Quartz Scheduler (used by Spring’s @Scheduled) extends the syntax
with:
- 6 fields — adds a leading seconds field (0–59).
L— last day of month / last weekday of month.W— nearest weekday to a given day-of-month (e.g.,15W= weekday closest to the 15th).#— nth weekday of the month (e.g.,2#1= first Monday).?— used in DoM or DoW when the other is specified, to explicitly opt out of the OR-trap behaviour.
The parser on this site is for standard 5-field cron; if you need
Quartz, check for ? or seconds in your input first.
What to do when cron isn’t enough
Cron is a great default, but it has limits:
- Sub-minute granularity: use systemd timers (Linux) or a polling loop in your application.
- Distributed scheduling (multiple servers, only one should run): Quartz, Temporal, or a job queue with a single dedicated worker.
- Catchup after downtime: cron skips missed runs by default.
systemd timers with
Persistent=truecatch up. Anacron is designed for this. - Job history / observability: plain cron logs to
/var/log/cronif anywhere; use a job runner for visibility.
Try the parser
The cron parser on this site handles all of standard cron syntax, shows next-fire times in your local timezone, and surfaces the DoM-or-DoW trap by listing actual fire dates so you can verify. Plus keyboard shortcuts, presets, and copy-to-clipboard.
Related across the network
- date.tooljo.com — for the date math when computing job intervals or downtime catchup.
- epoch.tooljo.com — convert cron-emitted timestamps back to human-readable dates.
- date.tooljo.com/blog/dst-and-date-math — why cron jobs at 02:30 misbehave twice a year.