Blogchevron_rightDevOps
DevOps

Cron Expression Cheatsheet — All Patterns Explained

Cron is fifty years old and still everywhere — but the syntax has hidden traps. This cheatsheet covers every pattern, gotcha, and platform difference you will hit in production.

November 30, 2026·9 min read·Build cron expressions →

The Short Answer

Standard cron is five fields separated by spaces. Each field maps to a position in time. The expression fires when all five match the current moment.

# ┌───────────── minute        (0 - 59)
# │ ┌─────────── hour          (0 - 23)
# │ │ ┌───────── day-of-month  (1 - 31)
# │ │ │ ┌─────── month         (1 - 12)
# │ │ │ │ ┌───── day-of-week   (0 - 6, Sun=0)
# │ │ │ │ │
# * * * * *  command_to_run

# Example: every weekday at 9:30am
30 9 * * 1-5
  • arrow_rightPosition 1: minute (0-59)
  • arrow_rightPosition 2: hour (0-23)
  • arrow_rightPosition 3: day-of-month (1-31)
  • arrow_rightPosition 4: month (1-12 (or JAN-DEC))
  • arrow_rightPosition 5: day-of-week (0-6 (Sun=0, or SUN-SAT))

Special Characters

Each field accepts five operator characters. Master these and you can express any schedule classical cron supports:

CharMeaningExample
*Any value* * * * * — every minute
,List of values0 9,12,15 * * * — at 9am, 12pm, 3pm
-Range0 9-17 * * 1-5 — every hour 9-5 weekdays
/Step*/15 * * * * — every 15 minutes
?No specific value (Quartz only)0 12 ? * MON — used to avoid OR conflict
LLast (Quartz only)0 0 L * ? — midnight on last day of month

The 12 Most Common Patterns

Most production cron expressions are some variation on these twelve. Bookmark this table and you will rarely need to think from scratch:

DescriptionExpressionNext fire
Every minute* * * * *In 1 minute
Every 5 minutes*/5 * * * *12:00, 12:05, 12:10...
Every 15 minutes*/15 * * * *12:00, 12:15, 12:30, 12:45
Every hour on the hour0 * * * *13:00, 14:00, 15:00...
Every 2 hours0 */2 * * *12:00, 14:00, 16:00...
Every day at midnight0 0 * * *Tomorrow 00:00
Every day at 9am0 9 * * *Tomorrow 09:00
Weekdays at 9am0 9 * * 1-5Next weekday 09:00
Weekends at 10am0 10 * * 6,0Next Sat or Sun 10:00
First day of every month0 0 1 * *1st of next month 00:00
Every Monday at 8am0 8 * * 1Next Monday 08:00
Quarterly (Jan/Apr/Jul/Oct 1st)0 0 1 */3 *1st of next quarter 00:00

Day-of-Month vs Day-of-Week — The OR Trap

The single most surprising behavior in cron: when both day-of-month (field 3) and day-of-week (field 5) are set to specific values, they are combined with OR, not AND.

# Naive read: "the 1st of the month AND a Monday"
# Actual:     "the 1st of the month OR any Monday"
0 9 1 * 1
# This fires on every 1st of the month AT 9am
# AND every Monday at 9am — not just when both align!

This is one of the oldest documented cron gotchas, dating back to the original V7 Unix implementation. POSIX standardized the OR behavior. If you actually want AND semantics — fire only when the 1st falls on a Monday — you cannot express it in standard cron. Quartz solves this with the ? character, which means "ignore this field"; you set whichever field you do not care about to ?.

The safe rule: never set both field 3 and field 5 to non-* values unless you actually want OR. Pick one or the other.

Time Zones

Standard cron uses the system timezone. On a server set to America/Los_Angeles, 0 9 * * * fires at 9am Pacific. Move that same crontab to a server in UTC and it now fires at 9am UTC — a 7 or 8 hour shift depending on DST.

Three problems compound: First, container images and most cloud servers default to UTC, so a developer who tested in their local TZ ships a job that fires at the wrong time. Second, daylight saving transitions create double-fires (2:30am gets repeated in fall) and skipped fires (2:30am does not exist in spring). Third, GitHub Actions and most cloud schedulers force UTC and refuse to read the system TZ.

The defensive pattern: always write cron expressions in UTC and document the local intent in a comment. 0 17 * * * # 9am Pacific (UTC-8) beats trying to make the scheduler timezone-aware.

Quartz Format

Quartz is the cron dialect used by Java schedulers — Spring's @Scheduled, Apache Airflow (in some operators), Quartz Scheduler itself. It extends standard cron with a seconds field and an optional year field, giving 6 or 7 fields total:

# Quartz 7-field format
# seconds  minutes  hours  day-of-month  month  day-of-week  year
0          0        9      ?             *      MON-FRI      *
# Every weekday at 9:00:00 AM, every year

Quartz also adds three power operators absent from POSIX cron:

  • arrow_rightL — last. L in day-of-month means last day; 5L in day-of-week means last Friday.
  • arrow_rightW — nearest weekday. 15W means the weekday closest to the 15th.
  • arrow_right# — nth occurrence. 2#1 in day-of-week means the first Monday of the month.

Pasting a Quartz expression into a POSIX crontab will produce a parse error. Pasting a POSIX expression into Quartz often silently runs at the wrong time because the leading field is interpreted as seconds, not minutes. Always verify the dialect first.

Platform Differences

The single biggest source of cron bugs in 2026 is assuming all platforms parse the same syntax. They do not. Here are the ones you will actually encounter:

PlatformFieldsSecondsTime zoneNotes
Linux crontab5NoSystem TZNo L, W, or # special chars
GitHub Actions5NoUTC onlyMin interval 5 min; jobs may delay during peak load
Kubernetes CronJob5NoUTC by default; spec.timeZone in v1.27+concurrencyPolicy: Allow / Forbid / Replace
AWS EventBridge6NoUTCYear field added; ? required for one of day fields
Quartz (Spring)6 or 7YesJVM defaultL, W, #, and ? supported

Common Mistakes

warning
Confusing 0 and *: In a cron field, 0 means "exactly zero," * means "every value." 0 * * * * fires once an hour at minute 0; * * * * * fires every minute. Off-by-N-thousand-fires bugs all start here.
warning
Off-by-one on day-of-month: day-of-month is 1-indexed (1-31). day-of-week and month are 0/1-indexed depending on platform. Forgetting which is which leads to schedules that skip a day or fire on Sunday when you meant Monday.
warning
Step values that do not divide evenly: */7 * * * * does NOT fire every 7 minutes — it fires at minutes 0, 7, 14, 21, 28, 35, 42, 49, 56, then jumps to 0 of the next hour (a 4-minute gap). Always pick a divisor of 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) for sub-hour intervals.
warning
Leap year and Feb 29: 0 0 29 2 * fires once every four years. If your monitoring expects a fire each year you will get a false alarm three years out of four. Use a calendar-day check inside the job instead.
warning
Assuming "@reboot" exists: @reboot is a Linux crontab extension that runs on system boot. It does not exist in Kubernetes CronJobs, GitHub Actions, or AWS EventBridge. Code defensively for the platform you will deploy to.

Generating Cron Programmatically

In a UI that lets users pick a schedule, building cron expressions from typed options beats free-text input every time. Here is a small TypeScript builder that covers the cases users actually want — with type safety and a guard against the "step that does not divide evenly" mistake:

// Type-safe cron generator for common cases
type CronOptions =
  | { type: 'every'; minutes: number }
  | { type: 'hourly'; minute?: number }
  | { type: 'daily'; hour: number; minute?: number }
  | { type: 'weekly'; day: 0|1|2|3|4|5|6; hour: number; minute?: number }
  | { type: 'monthly'; day: number; hour: number; minute?: number };

function buildCron(opts: CronOptions): string {
  switch (opts.type) {
    case 'every':
      if (60 % opts.minutes !== 0) {
        throw new Error('minutes must divide evenly into 60');
      }
      return `*/${opts.minutes} * * * *`;
    case 'hourly':
      return `${opts.minute ?? 0} * * * *`;
    case 'daily':
      return `${opts.minute ?? 0} ${opts.hour} * * *`;
    case 'weekly':
      return `${opts.minute ?? 0} ${opts.hour} * * ${opts.day}`;
    case 'monthly':
      return `${opts.minute ?? 0} ${opts.hour} ${opts.day} * *`;
  }
}

buildCron({ type: 'every', minutes: 15 });        // "*/15 * * * *"
buildCron({ type: 'daily', hour: 9, minute: 30 }); // "30 9 * * *"
buildCron({ type: 'weekly', day: 1, hour: 8 });    // "0 8 * * 1"

Wrapping cron in a typed builder eliminates four classes of bugs at the type-system level: invalid day-of-week values, missing minute fields, illegal step intervals, and the OR-trap (because the user never sets both day fields). For the cases this builder cannot express — multi-time daily, complex skip patterns — fall back to free text but validate it with a parser like cron-parser before saving.

Build cron expressions visually

Pick a schedule, see the expression and the next 10 fire times — copy-ready for any platform.

Open Cron Generator →

Related Tools

Cron Expression Cheatsheet — All Patterns