Skip to main content

How to Read a Cron Expression: Decode the Five Fields, the Stars, and the Slashes

A practical guide to reading any cron expression. Learn the five fields, what * / , - mean, the day-of-week 0-vs-7 trap, and how to preview the next runs before you ship a broken schedule.

Published By 李雷
#cron #crontab #scheduling #devops #linux

How to Read a Cron Expression: Decode the Five Fields, the Stars, and the Slashes

I once deleted a line that read 15 2 * * 0 from an inherited /etc/crontab because I assumed it was a daily job cluttering the file. It was the weekly off-site backup. Nobody noticed for eleven days, which is exactly how long it took for someone to need a restore. That afternoon taught me to read cron expressions properly instead of guessing, and this guide is the reference I wish I'd had open in another tab.

A cron expression is a compact schedule. Five tokens separated by spaces tell a daemon when to fire a job. The syntax looks cryptic only because it's terse — once you know the five positions and the four operators, you can read almost anything. Let's take it apart.

The Five Fields, Left to Right

Classic POSIX cron uses exactly five whitespace-separated fields, always in this order:

┌───── minute        (0–59)
│ ┌──── hour         (0–23)
│ │ ┌─── day of month (1–31)
│ │ │ ┌── month       (1–12)
│ │ │ │ ┌─ day of week (0–7)
│ │ │ │ │
* * * * *

So 30 8 * * * reads field by field: minute 30, hour 8, any day of month, any month, any day of week — "at 08:30 every day." The order is fixed and unforgiving. The single most common mistake I see is swapping day-of-month and day-of-week, because both are near the right end. 0 0 * * 1 is every Monday, not the first of the month. The first of the month is 0 0 1 * *. If you ever doubt which one you wrote, paste both into the Cron Expression Explainer and compare the next-run lists side by side — the Monday version lands on different calendar dates than the 1st-of-month version, and the difference is obvious in two seconds.

This five-field layout traces directly back to Paul Vixie's cron, written in 1987 and shipped with most Linux and BSD distributions ever since. The field definitions are codified in the man 5 crontab page (the POSIX crontab spec), so when this guide says "minute is 0–59," that range is the same one your production daemon enforces.

The Four Operators: * , - /

Inside any of the five fields you can use four operators. Master these and you've mastered 95% of cron.

  • * (asterisk) means every valid value. In the minute field it means every minute; in the month field, every month.
  • , (comma) is a list. 0,15,30,45 in the minute field means "at 0, 15, 30, and 45 minutes past the hour."
  • - (hyphen) is an inclusive range. 9-17 in the hour field means hours 9 through 17, all nine of them.
  • / (slash) is a step. */5 in the minute field means "every 5th minute" — 0, 5, 10, and so on. You can step inside a range too: 0-30/10 means 0, 10, 20, 30.

These combine. 0 9-17 * * 1-5 means "at minute 0, for hours 9 through 17, Monday through Friday" — standard business-hours alerting. Reading it cold, you'd say it fires hourly on the hour from 9 AM to 5 PM on weekdays.

A Real Example, Decoded End to End

Take this expression, which I really did find in a project's deploy hooks:

0 9-17 * * 1-5

Field by field: minute 0, hour 9-17 (a range, nine hours), day-of-month * (any), month * (any), day-of-week 1-5 (Monday through Friday). The plain-English reading: "At the top of every hour from 09:00 to 17:00, Monday through Friday."

Now the part that catches people: the next few fire times. Suppose you're reading this on a Friday afternoon. The upcoming runs are roughly:

Fri 16:00  (+1h)
Fri 17:00  (+1h)
Mon 09:00  (+64h)   ← the weekend gap
Mon 10:00  (+1h)
Mon 11:00  (+1h)

That 64-hour jump from Friday 17:00 to Monday 09:00 is the schedule visibly skipping Saturday and Sunday. Seeing the gap printed out is far more reassuring than mentally simulating it — you know it skips the weekend rather than hoping it does. This is why a next-run preview matters: the expression tells you the rule, but the concrete timestamps tell you the behavior. Because those times are computed in your browser's local timezone, they match what cron on your own machine would do; if you need them in another zone, drop the first fire time into the Timezone Converter.

The Day-of-Week 0-vs-7 Trap

Here's the gotcha that has burned more engineers than any other field: the day-of-week column accepts both 0 and 7 for Sunday. Vixie cron treats 0 and 7 as the same day so that people from either convention feel at home. So 0 0 * * 0 and 0 0 * * 7 both mean "midnight on Sunday."

The trap is the boundary. Valid values are 0 through 7. Write 0 0 * * 8 and you've gone off the end — 8 is not a weekday, and a strict parser rejects it while a lenient one may silently do something surprising. Likewise the months and days you allow have to actually line up: 0 0 31 2 * asks for February 31st, a date that does not exist, so a real cron daemon runs it zero times all year and never tells you. 0 0 30 2 * is subtler — February 30th never exists either, but the closely related 0 0 29 2 * only fires in leap years. These are the failures that don't error loudly; they just quietly never run, and you discover the missing report during a quarterly audit. A good explainer flags "this expression will never fire" up front instead of returning an empty list you might mistake for a parsing hiccup.

Common Schedules Worth Memorizing

A handful of expressions cover most real-world needs, and recognizing them on sight speeds up every code review:

  • * * * * * — every minute (use sparingly; this hammers a job 1,440 times a day).
  • */5 * * * * — every 5 minutes, and because 5 divides 60 cleanly, the cadence is even.
  • 0 * * * * — every hour, on the hour.
  • 0 3 * * * — daily at 03:00, a classic for nightly batch work.
  • 0 0 * * 0 — weekly, Sunday midnight.
  • 0 0 1 * * — monthly, on the 1st.
  • 0 0 1 1 * — yearly, New Year's midnight.

One caution on steps: */N only behaves evenly when N divides the field's range cleanly. */7 * * * * looks like "every 7 minutes," but 60 isn't divisible by 7, so it fires at :00, :07, :14 … :56, then snaps back to :00 of the next hour with only a 4-minute gap. If you genuinely need an even cadence, pick a divisor of 60 — */15 or an explicit 0,15,30,45. The drift is invisible in the text and obvious the moment you look at the actual run times.

When you're writing a schedule from scratch rather than decoding one, the click-through builder over at Crontab Helper gives you guardrails so you don't fat-finger a field; this guide and the explainer are for the reverse job — staring at someone else's expression and needing to know what it does before you touch it.

Read Before You Delete

The discipline that backup incident taught me is simple: never delete or edit a cron line you can't read aloud. Walk the five fields left to right, expand each operator, then confirm with a concrete next-run list before you commit. The syntax is small enough to learn in an afternoon and sharp enough to cost you a weekend if you guess. Decode it properly, and 15 2 * * 0 stops being a mystery and starts being "the weekly Sunday backup, 02:15" — which is exactly the kind of line you leave alone.


Made by Toolora · Updated 2026-06-13