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:
| Expression | Reads as |
|---|---|
0 9 * * 1-5 | At 9:00 AM on weekdays |
*/15 * * * * | Every 15 minutes |
0 0 1 * * | First day of every month at midnight |
0 22 * * 5 | 10: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.
Special characters
Inside any field, these symbols change the meaning:
| Symbol | Meaning | Example |
|---|---|---|
* | Every value | * * * * * = every minute |
, | List of values | 0,15,30,45 = every 15 minutes (explicit) |
- | Range | 9-17 = 9 AM through 5 PM |
/ | Step | */5 = every 5 minutes |
? | No specific value (Quartz/AWS only) | Required when one day field is set |
L | Last (Quartz only) | L in day-of-month = last day of month |
W | Weekday 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:
| Expression | Meaning |
|---|---|
0-30/5 | Every 5 minutes from 0 to 30 (0, 5, 10, 15, 20, 25, 30) |
1,15,30 | At minutes 1, 15, and 30 |
1-5,15,30 | At minutes 1 through 5, plus 15 and 30 |
9-17/2 | Every 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:
| Macro | Expands to | Meaning |
|---|---|---|
@yearly / @annually | 0 0 1 1 * | Once a year, midnight Jan 1 |
@monthly | 0 0 1 * * | First of the month, midnight |
@weekly | 0 0 * * 0 | Sunday midnight |
@daily / @midnight | 0 0 * * * | Every day at midnight |
@hourly | 0 * * * * | 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.