Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

Cron syntax — every field, every quirk

On this page
  1. The five fields
  2. What each field accepts
  3. Field-by-field
    1. Minute (0–59)
    2. Hour (0–23)
    3. Day of month (1–31)
    4. Month (1–12 or JAN–DEC)
    5. Day of week (0–6 or SUN–SAT)
  4. The three traps
    1. Trap 1: day-of-month OR day-of-week
    2. Trap 2: missed runs and DST
    3. Trap 3: long-running jobs and overlap
  5. Special strings
  6. Quartz extensions (don’t apply to standard cron)
  7. What to do when cron isn’t enough
  8. Try the parser
  9. Related across the network

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:

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:

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.

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:

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:

StringEquivalent
@yearly / @annually0 0 1 1 *
@monthly0 0 1 * *
@weekly0 0 * * 0
@daily / @midnight0 0 * * *
@hourly0 * * * *
@rebootAt 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:

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:

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.