A cron expression tells a scheduler exactly when to run a job. It consists of five fields: minute, hour, day of month, month, and day of week, separated by spaces. Reading it correctly requires understanding how each field filters time and how special characters combine those filters.
Understanding the Five Fields
Every standard cron expression has exactly five fields, read left to right. Each field specifies a range or list of values for that time unit. The scheduler fires the job when the current time matches all five fields simultaneously.
The fields are ordered as follows: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–7, where both 0 and 7 represent Sunday). A value of * means "every possible value" for that field. Comma-separated lists like 1,15,30 mean "only these specific values." Hyphenated ranges like 9-17 mean "every integer from the first number through the second, inclusive."
Consider this expression: */15 9-17 * * 1-5. Breaking it down field by field:
*/15in the minute field means every multiple of 15 minutes starting at 0 (so :00, :15, :30, :45).9-17in the hour field means hours 9 through 17 inclusive.*in the day-of-month field means every day of the month.*in the month field means every month.1-5in the day-of-week field means Monday through Friday.
Put together, this fires every 15 minutes between 9 AM and 5 PM on weekdays. At 9:00 AM Monday it fires. At 9:15 AM Monday it fires again. At 5:45 PM Friday it fires. At 5:45 PM Saturday it does not fire, because Saturday is day 6, outside the 1–5 range.
Decoding Special Characters
Three special characters appear frequently in cron expressions and change how values are interpreted. Understanding them prevents most scheduling surprises.
The asterisk * matches every possible value for its field. In 0 * * * *, the hour field uses *, so the job runs at minute 0 of every hour, every day, every month, every weekday.
The slash / creates a step interval. */15 in the minute field means start at the beginning of the range and increment by 15. You can also combine a range with a step: 10-30/5 means start at minute 10, go up to minute 30, stepping by 5 — so minutes 10, 15, 20, 25, and 30.
The comma , creates an explicit list. 0,30 in the minute field means only minute 0 and minute 30. This is useful when you want irregular intervals that a simple step cannot express.
Here is a worked example combining all three: 0,30 */2 * * *. The minute field 0,30 fires at :00 and :30. The hour field */2 fires at hours 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, and 22. The remaining fields are *, so every day and month. This job runs twice per hour, but only during even-numbered hours. At 9:00 AM it does not fire because hour 9 is odd. At 10:00 AM it does fire.
Interpreting Nicknames and Shorthands
Some cron implementations support shorthand nicknames that replace a full five-field expression. These are convenience aliases, not a separate syntax. The most common ones map directly to standard patterns:
| Nickname | Equivalent Expression | Plain English |
|---|---|---|
@hourly | 0 * * * * | Every hour at minute 0 |
@daily | 0 0 * * * | Every day at midnight |
@weekly | 0 0 * * 0 | Every Sunday at midnight |
@monthly | 0 0 1 * * | First day of every month at midnight |
@yearly | 0 0 1 1 * | January 1 at midnight |
@reboot | (implementation-specific) | Once when the system starts |
Not every cron implementation supports every nickname. Vixie cron supports all of the above. Some minimal implementations support only a subset. If you are writing a crontab for a shared server, check which implementation runs there. When in doubt, write the full five-field expression instead of relying on a nickname. The full form works everywhere.
Verifying Your Schedule Logic
After writing a cron expression, confirm it fires when you expect. The quickest method is to mentally test a few edge cases against your expression.
Take */15 9-17 * * 1-5 again. Ask yourself: does this fire at 8:45 AM? No, because hour 8 is outside the range 9–17. Does it fire at 5:00 PM? Yes, hour 17 is included in the range. Does it fire at Saturday noon? No, Saturday is day 6, outside 1–5. Does it fire at Monday 9:30 AM? Yes, minute 30 is a multiple of 15, hour 9 is in range, Monday is day 1.
For more complex expressions, especially those mixing steps and lists, a visual builder helps. You pick values from dropdowns for minute, hour, day, month, and weekday, and see the resulting expression update live. This catches off-by-one errors in ranges and confirms that your step intervals align with your intended frequency. If you want to see exactly when your expression next fires in your local timezone before deploying it, paste it into CronExplain to get a plain-English sentence and the next ten firing times computed in your browser.
Common Mistakes to Avoid
Several patterns cause more confusion than others. Learning to spot them saves debugging time later.
The first mistake is mixing up day-of-month and day-of-week logic. When both fields have non-* values, the job fires when either condition is met, not both. So 0 9 1 * 1 fires at 9 AM on the first of every month or every Monday, whichever comes first. To require both conditions simultaneously, you need a wrapper script or a different scheduling approach.
The second mistake is assuming */5 in the hour field means every five hours starting at midnight. It does. Hours 0, 5, 10, 15, and 20 fire. Hour 23 does not. If you want every hour regardless, use * instead.
The third mistake is forgetting that cron uses the server's local timezone unless configured otherwise. If your server is in UTC and your team is in New York, a job set for 0 9 * * * fires at 9 AM UTC, which is 4 AM Eastern. Check your server timezone with timedatectl or date before assuming the offset.
Using Visual Tools for Clarity
Reading cron expressions becomes faster with practice, but even experienced operators double-check complex schedules. A visual builder lets you click through minute, hour, day, month, and weekday fields and see the resulting expression assemble itself. This is particularly helpful when you are translating a human requirement like "every weekday morning at half past nine" into 30 9 * * 1-5.
For verification, seeing the next several firing times removes ambiguity about whether your expression handles edge cases correctly. Does */15 9-17 * * 1-5 really skip Saturday? Does 0 */2 * * * really skip odd hours? The next-fire preview answers these questions immediately, in your local timezone, without requiring you to mentally simulate the calendar.
When you build a schedule you will reuse across projects, save it with a name and comment. That way, next quarter when you need "every five minutes on weekdays" again, you pull it from your library instead of reconstructing the expression from scratch.