A cron line looks like five numbers and some punctuation, and that compactness is exactly why it trips people up. One field in the wrong position, one operator you half-remember, and your job runs a thousand times a day instead of once. This guide walks through what each part means, the operators that combine them, and the one matching rule that surprises almost everyone.
The five fields
A standard cron expression is five fields separated by spaces, read left to right:
* * * * *
| | | | |
| | | | +-- day of week (0-7, where 0 and 7 both mean Sunday)
| | | +---- month (1-12)
| | +------ day of month (1-31)
| +-------- hour (0-23)
+---------- minute (0-59)
Each field answers “on which values of this unit should the job fire?” The minute field accepts 0 through 59, the hour field 0 through 23, day of month 1 through 31, month 1 through 12, and day of week 0 through 7. Sunday is available as both 0 and 7, which lets you write weekends either way.
A job runs when the current time matches every field at once (with one important exception covered below). So 30 8 * * * means minute 30, hour 8, any day of month, any month, any day of week: 8:30 AM every day.
The operators
Four operators do all the work, and every one of them is plain ASCII: asterisk, comma, hyphen, and slash.
- Asterisk (
*) means “every value” for that field. In the minute field it means every minute; in the month field, every month. - Comma (
,) lists specific values.0,15,30,45in the minute field fires at four points each hour. - Hyphen (
-) defines an inclusive range.1-5in the day-of-week field means Monday through Friday. - Slash (
/) defines a step.*/15in the minute field means “every 15 minutes starting from 0”: 0, 15, 30, 45. You can also step within a range:0-30/10gives 0, 10, 20, 30.
Operators combine inside a single field. 0,30 9-17 * * * runs at minute 0 and minute 30 of every hour from 9 AM through 5 PM. If that made you pause, the cron to English translator turns any expression back into a sentence.
The day-of-month vs day-of-week gotcha
Here is the rule that catches experienced engineers. Every field is combined with logical AND, except for the two day fields. When you restrict both day of month and day of week, cron uses OR, not AND.
That means 0 0 1 * MON does not run “on the 1st only if it is a Monday.” It runs on the 1st of every month AND on every Monday, firing far more often than the naive reading suggests. This behavior is baked into the classic Vixie cron implementation and inherited by most systems.
The practical rule: use one day field or the other, not both, unless you genuinely want the union. If you need “the first Monday of the month,” cron alone cannot express it cleanly, so you handle that logic inside the job instead. When a schedule looks right but fires on unexpected days, this rule is the first suspect. Feeding it to cron next run times makes the surprise visible, because you see the actual dates it will fire.
Named months and days
You do not have to memorize numbers. Most cron implementations accept three-letter names for months (JAN through DEC) and days of the week (SUN through SAT). They are case-insensitive, so mon and MON both work.
Names make intent obvious. 0 9 * * MON-FRI reads almost like English: 9 AM, Monday through Friday. Compare that to 0 9 * * 1-5, which is identical in effect but forces you to remember that 1 is Monday. One caveat: names cannot be used with steps in some implementations, so JAN-DEC/2 may be rejected where 1-12/2 works. When portability matters, prefer numbers.
Worked examples
Reading a few real expressions cements the pattern.
0 0 * * *fires at minute 0 of hour 0, every day: midnight, daily. This is the canonical “run once a day” schedule.*/15 * * * *fires every 15 minutes, all day, every day. The step operator in the minute field expands to 0, 15, 30, 45, and the asterisks let every hour and date through.0 9 * * MON-FRIfires at 9:00 AM on weekdays only. Minute 0, hour 9, any day of month, any month, and the day-of-week range Monday through Friday. Because only one day field is restricted, the OR gotcha does not apply here.
Two more worth recognizing on sight: 0 */6 * * * runs every six hours (midnight, 6 AM, noon, 6 PM), and 30 2 1 * * runs at 2:30 AM on the first of each month.
Putting it together
Cron rewards a slow read. Take each field in order, decode its operators, and remember that the two day fields play by their own OR rule. If you are writing a schedule from scratch rather than decoding one, the crontab builder assembles the five fields from plain choices so you never fight the syntax by hand. Between building an expression, translating it to English, and previewing the next run times, the guesswork disappears.