Foundations

Cron syntax, explained.

Cron expressions look cryptic but follow simple rules. This guide walks through every field, every special character, and the edge cases that bite even experienced developers. After reading, you should be able to read or write any standard cron expression from memory.

Anatomy of a cron expression

A standard Unix cron expression is five fields separated by whitespace:

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

Each field accepts one or more values. The asterisk (*) means "every value in the allowed range." So * * * * * means "every minute of every hour of every day of every month on every weekday" — which is just "every minute."

Here are a few read-out-loud examples:

ExpressionReads as
0 9 * * 1-5At 9:00 AM on weekdays
*/15 * * * *Every 15 minutes
0 0 1 * *First day of every month at midnight
0 22 * * 510:00 PM every Friday

The five fields, in detail

1. Minute (0–59)

The minute within the hour. 0 means "on the hour"; 30 means "at half past." When stepped (*/5), counts from 0: 0, 5, 10, 15…

2. Hour (0–23)

24-hour clock. 0 is midnight, 12 is noon, 23 is 11 PM. There is no 12-hour or AM/PM concept in cron itself.

3. Day of month (1–31)

Calendar day. Cron silently does the right thing for short months — 0 0 31 * * will simply skip months that don't have a 31st rather than firing on the 1st of the next month.

4. Month (1–12)

Calendar month. 1 is January, 12 is December. Accepts named values like JAN, FEB, DEC (case-insensitive).

5. Day of week (0–6)

Most systems use 0=Sunday, 1=Monday, … 6=Saturday. Some implementations also accept 7 for Sunday (Vixie cron, the default on most Linux systems). Accepts named values: SUN, MON, … SAT.

⚠ Day-of-month and day-of-week are linked. If you set both day-of-month and day-of-week, Unix cron fires when either matches (it's an OR, not an AND). This trips up everyone at least once.

Special characters

Inside any field, these symbols change the meaning:

SymbolMeaningExample
*Every value* * * * * = every minute
,List of values0,15,30,45 = every 15 minutes (explicit)
-Range9-17 = 9 AM through 5 PM
/Step*/5 = every 5 minutes
?No specific value (Quartz/AWS only)Required when one day field is set
LLast (Quartz only)L in day-of-month = last day of month
WWeekday nearest a date (Quartz only)15W = weekday closest to the 15th
#Nth weekday of month (Quartz only)2#1 = first Monday

Standard Unix cron only supports * , - /. The rest are Quartz/AWS extensions — see our Quartz vs Unix cron guide for details.

Combining special characters

You can mix them inside a single field:

ExpressionMeaning
0-30/5Every 5 minutes from 0 to 30 (0, 5, 10, 15, 20, 25, 30)
1,15,30At minutes 1, 15, and 30
1-5,15,30At minutes 1 through 5, plus 15 and 30
9-17/2Every 2 hours from 9 AM to 5 PM

Named values for months and weekdays

Most modern cron implementations accept three-letter abbreviations for months and days:

0 0 * JAN-MAR MON-FRI    # weekdays in Q1
0 9 * * MON,WED,FRI      # MWF at 9 AM

The names are case-insensitive. Some old cron versions only accept numbers — when in doubt, stick to numbers for portability.

Spring/Vixie macros

Most cron implementations recognize shorthand macros that expand to full expressions:

MacroExpands toMeaning
@yearly / @annually0 0 1 1 *Once a year, midnight Jan 1
@monthly0 0 1 * *First of the month, midnight
@weekly0 0 * * 0Sunday midnight
@daily / @midnight0 0 * * *Every day at midnight
@hourly0 * * * *Top of every hour
@reboot(no equivalent)Once at system startup

AWS EventBridge and GitHub Actions do not support macros. Quartz uses different macro names (0 0 0 * * ? rather than @daily).

The gotchas

1. Day-of-month and day-of-week behave OR, not AND

This is the most-tripped-over edge case. Consider:

0 0 15 * 1   # Midnight on the 15th, AND midnight on Mondays

Many developers expect "midnight on the 15th, but only if it's a Monday." Cron does the opposite: it fires on BOTH the 15th of any month AND every Monday. To get the AND semantics, you have to check inside your script with [[ $(date +\%u) == 1 ]] && actually_run.

2. Cron runs in the system's timezone, which is often UTC

On most production servers, the timezone is UTC. So 0 9 * * 1-5 means 9 AM UTC, which is 1 AM Pacific or 5 PM Tokyo. To run "9 AM Pacific" on a UTC server, use 0 17 * * 1-5 (since 9 AM PST = 17:00 UTC) — but this drifts during daylight saving. See our DST guide.

3. The minimum interval is 1 minute

Standard cron has no sub-minute granularity. Spring's @Scheduled and Azure NCRONTAB add a seconds field. AWS EventBridge enforces a 1-minute minimum. GitHub Actions enforces 5 minutes.

4. */N doesn't always do what you expect

*/5 in the minute field means "every 5 minutes starting at 0: 0, 5, 10…" not "every 5 minutes from now." In the day-of-month field, */5 means "days 1, 6, 11, 16, 21, 26, 31" — note that it resets at the start of each month, so the interval is not a clean 5-day cycle across month boundaries.

5. @reboot isn't universal

It works on most Linux distributions but is missing from BSD-style cron. Always test before relying on it.

Quick reference

Pin this somewhere visible if you write cron expressions occasionally:

┌───────── minute (0 - 59)
│ ┌─────── hour (0 - 23)
│ │ ┌───── day of month (1 - 31)
│ │ │ ┌─── month (1 - 12, or JAN-DEC)
│ │ │ │ ┌─ day of week (0 - 6, Sun=0, or SUN-SAT)
│ │ │ │ │
* * * * *

Special chars in a field:
  *       any value
  ,       list separator (1,3,5)
  -       range (1-5)
  /       step (*/5 = every 5)

Macros:
  @yearly  = 0 0 1 1 *
  @monthly = 0 0 1 * *
  @weekly  = 0 0 * * 0
  @daily   = 0 0 * * *
  @hourly  = 0 * * * *

Or use the explainer to paste any expression and get a plain-English translation instantly.

Related

Continue reading.