Someone hands you 0 */2 * * 1-5 and asks "when does that run?" You can read the five fields, but the slash and the dash turn it into a small puzzle. Cron syntax is compact by design — five fields and four symbols cover almost every schedule you'll ever need — but compact means easy to misread.
The good news: a cron expression is fully decodable once you know what each field and symbol does. This guide reads one field by field. If you'd rather just paste an expression and see the answer in plain English, the Skojio cron expression helper translates it and lists the next 10 fire times instantly.
The five fields of a cron expression, left to right
A standard cron line is five space-separated fields followed by the command:
┌───────── 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; 0 and 7 are both Sunday)
│ │ │ │ │
0 9 * * 1 /path/to/job.sh
Read positionally, never by name — cron doesn't label its fields, so 9 in the second slot is the hour, full stop. The example above means 09:00, on Mondays. Everything else is built from how each field is filled.
Count the fields first. Five means standard cron; six usually means a seconds field is bolted on the front (common in Quartz and some app schedulers). Misjudging which dialect you're in is the fastest way to read every field one position out.
How to read *, ,, -, and / in any cron field
Four symbols do all the work, and they mean the same thing in every field:
-
*(asterisk) — every valid value.*in the hour field means every hour.* * * * *runs once a minute. -
,(comma) — a list.0,30in minutes means "at 0 and at 30".1,15in day-of-month means the 1st and the 15th. -
-(dash) — an inclusive range.1-5in day-of-week means Monday through Friday.9-17in hours means every hour from 9am to 5pm. -
/(slash) — a step.*/15in minutes means every 15th minute (0, 15, 30, 45). You can combine it with a range:0-30/10means minutes 0, 10, 20, 30.
The slash trips people up most. */15 is fixed clock positions, not an offset from when you saved the crontab — if you save at 14:07, the next run is 14:15, not 14:22. The same logic governs the three cron mistakes that break overnight jobs, so it's worth internalising early.
What 0 9 * * 1-5 means in plain English (worked example)
Take it field by field:
| Field | Value | Reads as |
|---|---|---|
| minute | 0 |
at minute 0 |
| hour | 9 |
of the 9th hour (09:00) |
| day-of-month | * |
every day of the month |
| month | * |
every month |
| day-of-week | 1-5 |
Monday to Friday |
Put together: 09:00 every weekday. A classic "send the morning report on business days" schedule.
src="/blog/figures/cron-anatomy.jpg"
alt="The cron expression 0 9 * * 1-5 broken into five labelled fields — minute, hour, day, month and weekday — reading as 09:00 Monday to Friday"
caption="Reading 0 9 * * 1-5 field by field: 09:00, Monday to Friday."
/>
Now read 0 */2 * * 1-5 the same way: minute 0, every 2nd hour (00:00, 02:00, 04:00 … 22:00), every weekday — so on the hour, every two hours, Monday to Friday. Reading positionally and resolving each symbol turns any expression into a sentence.
src="/blog/figures/cron-explain-terminal.jpg"
alt="A terminal translating the cron expression 0 */2 * * 1-5 into plain English: at minute 0, every 2nd hour, Monday through Friday"
caption="The same reading, automated: paste an expression, get a sentence and the next run times."
/>
One more that looks harmless but isn't: 0 0 1,15 * * means midnight on the 1st and 15th of every month — a list, not a range. Swap the comma for a dash and 0 0 1-15 * * becomes midnight on every day from the 1st to the 15th, which is fifteen runs a month instead of two. The single character is the entire difference, which is exactly why reading each symbol deliberately beats skimming.
Special strings and the optional sixth field
Many cron implementations accept named shortcuts that replace all five fields:
-
@hourly=0 * * * * -
@daily(or@midnight) =0 0 * * * -
@weekly=0 0 * * 0 -
@monthly=0 0 1 * * -
@reboot= once, at daemon startup
You'll also see six-field expressions in the wild. Some schedulers (Quartz, Spring, many container cron images) prepend a seconds field, so */30 * * * * * means "every 30 seconds". If an expression has six fields and the first looks like a seconds step, that's why — count the fields before you read them, because a five-field parser will misread a six-field line entirely.
A couple of field-specific quirks are worth knowing too. Months and weekdays accept three-letter names: 0 0 * JAN MON is as valid as 0 0 * 1 1, and often clearer. Day-of-week is the field most likely to bite, because 0 and 7 both mean Sunday — so a range like 0-6 and 1-7 both cover the full week, just with the boundary in a different place. And ? shows up in Quartz-style expressions as a "no specific value" marker in the two day fields, used to sidestep the day-of-month-versus-day-of-week ambiguity that standard cron handles as an OR.
Read any cron expression in plain English
Reading by hand is a good skill, but the safest check before you ship a schedule is to confirm the next fire times match your intent.
The cron expression helper parses an expression, describes it in plain English, and lists the next 10 runs in your chosen timezone — which also catches the day-of-month-versus-day-of-week surprise that pure field-reading can miss. Because schedules drift across regions, pair it with the Skojio timezone helper when a job has to fire at a specific local time. Both run in your browser with no install.
Recap
| Symbol | Meaning | Example |
|---|---|---|
* |
every value |
* * * * * → every minute |
, |
list |
0,30 → at :00 and :30 |
- |
inclusive range |
1-5 → Mon–Fri |
/ |
step (fixed positions) |
*/15 → :00 :15 :30 :45 |
Five fields, four symbols, read left to right — that's the whole grammar. Once it's second nature you can decode most expressions at a glance, and when one looks ambiguous, drop it into the cron helper and let the next fire times confirm you read it right.
Top comments (0)