All posts

Cron expression examples, and what each one actually does

7 min read

Cron expressions are easier to read than to write. These are the ones that show up in real crontabs, including the pairs that differ by a single character and mean something else entirely.

Most cron questions are the same question. Someone has a line in front of them and wants to know what it does, or wants a schedule and does not know how to write it. Both are mechanical once the fields are in your head, and the mistakes are almost never in the fields. They are in the pairs of expressions that look like variations of each other and are not.

Here is the reference first, then the traps. For the field mechanics behind them rather than the examples, the five-field overview is the place to start.

The minute and hour ones

Expression Runs Note
* * * * * Every minute 1,440 a day. Fine for a liveness probe, expensive for anything that opens a database
*/5 * * * * Every 5 minutes 288 a day
*/15 * * * * Every 15 minutes The step divides 60, so the interval is even
0,15,30,45 * * * * Every 15 minutes Same schedule, written as a list. The two are interchangeable
15 * * * * At minute 15 of every hour 24 runs, not 96. The * in the minute field is the whole difference
0 * * * * Every hour on the hour What @hourly expands to
30 * * * * Every hour at half past
0 */2 * * * Every 2 hours Hours 0, 2, 4 … 22. Not "two hours from when it last ran"
0 0-23/6 * * * Every 6 hours 00:00, 06:00, 12:00, 18:00. Range plus step, same result as */6
0 0,12 * * * Twice a day Midnight and noon
*/10 9-18 * * * Every 10 minutes, 9am to 6pm The hour range only restricts when the job may start
0 9-17/2 * * 1-5 Every 2 hours on weekdays 09:00, 11:00, 13:00, 15:00, 17:00. Five runs a day, not nine

Daily, weekly, monthly

Expression Runs Note
0 0 * * * Midnight, every day @daily
0 6 * * * 06:00 every day
30 9 * * 1-5 09:30 on weekdays The weekday field is 1-5, Monday to Friday
0 8 * * 1-5 08:00 on weekdays
0 0 * * 0 Midnight on Sunday @weekly. Note the 0
0 0 * * 7 Midnight on Sunday Identical. 0 and 7 are both Sunday
0 0 * * 1,3,5 Midnight Monday, Wednesday, Friday
5 4 * * 1 04:05 on Monday Two fields from the left, then the day
0 0 1 * * Midnight on the 1st @monthly
0 12 1,15 * * Noon on the 1st and the 15th
0 0 1 */2 * Midnight on the 1st, every other month Months 1, 3, 5, 7, 9, 11
0 0 1 1 * Once a year @yearly

That is the shape of most crontabs: a minute and an hour, then a day constraint you rarely touch. The next four sections are where the day constraint stops behaving the way people expect.

Every other day doesn't exist

0 0 */2 * * is the expression people reach for when they want a job to run every two days. It does not do that. The step applies to the field's own range, and the day-of-month range is 1 to 31, so */2 selects the odd days: 1, 3, 5, 7 and so on.

Most of the year that looks like every other day. The problem is the months that end on an odd number:

2026-01-29   Thu
2026-01-31   Sat
2026-02-01   Sun   <- one day later, not two
2026-02-03   Tue

January has 31 days, February starts on the 1st, and both are odd. The job runs on consecutive days. Across 2026 the expression fires 186 times and produces six of those back-to-back pairs: at the end of January, March, May, July, August and October.

The same */2 works correctly one field over. 0 0 1 */2 * really is the 1st of every other month, because there are twelve months and twelve is even, so the cycle closes cleanly. Days are the field where the trick breaks.

There is no standard expression for "every 48 hours". Anything that genuinely needs a fixed interval has to keep state, usually a timestamp file the job checks before doing work, or a @reboot-style trigger plus a marker. Cron compares the current minute against a pattern; it has no idea when the job last ran.

Pairs that differ by one character

The line Reads as Its near neighbour
15 * * * * 24 runs a day */15 * * * * is 96. Moving the */ costs you a factor of four
0 0 * * 0 Midnight Sunday 0 0 * * 7 is the same line with a different number
0 0 1 * * The 1st of the month 0 0 1 * 0 adds every Sunday. Because day-of-month and day-of-week are ORed, that is 61 runs in 2026, including two pairs on consecutive days
0 0 */2 * * The odd days 0 0 1 */2 * is the odd months. The step means something different in each field
0 9-17 * * 1-5 Nine times on a weekday 0 9-17/2 * * 1-5 is five. The step inside the range halves it
0 0 1-15/3 * * Days 1, 4, 7, 10, 13 Not "every third day". It stops at 13 and starts over at the 1st

The OR rule in the third row is the one worth reading twice. Day-of-month and day-of-week are the only two fields combined with OR rather than AND, and only when both are restricted. The POSIX specification for crontab states it directly: the command runs on any day matching either field. 30 4 1,15 * 5 is 04:30 on the 1st, 04:30 on the 15th, and 04:30 every Friday, which in January 2026 gives you the 1st and the 2nd in a row.

Step values themselves are worth a note. POSIX defines exactly three constructs in a field: *, a comma-separated list, and a hyphen range. The / step is a BSD extension that Vixie cron and everything derived from it accept, and that the Linux man page documents. On Linux it is safe; if you are generating crontabs for something that implements the standard strictly, */15 may not survive the trip.

The first Monday problem

"First Monday of the month" has no expression in a Linux crontab. The # operator that does it, 0 0 * * 1#1, belongs to Quartz and AWS EventBridge, and L for the last day of the month belongs to the same two. On plain cron you run the job on every candidate day and let the shell decide:

0 4 * * 1 [ "$(date +\%d)" -le 7 ] && /usr/local/bin/weekly-report.sh

The job fires every Monday, and the guard lets it through only in the first seven days of the month, which is the same thing as the first Monday. The escaped \% is not decorative: cron rewrites an unescaped percent before the shell ever sees the line, which is covered in what a crontab file actually does.

The last day of the month follows the same shape, with a different test:

0 4 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/month-end.sh

Four candidate days, one of which is always the last. Both guards assume GNU date; on BSD and macOS the -d flag means something else, and you would use date -v+1d +%d.

Which platforms accept L, # and the rest of the extended symbols is on the cron cheat sheet, next to the field ranges.

Expressions that do not parse

Four fields, six fields, a value out of range, a step of zero. A parser that follows the documented grammar rejects all of these:

  • * * * * is four fields. The minimum is five.
  • 0 0/2 * * * uses a step on a bare number. Steps are allowed after * and after a range, and 0/2 is neither. If you have seen that line in a real crontab it was either accepted by a lenient parser or, more often, quietly doing nothing. Write 0 */2 * * *.
  • 0 0 * * 8 is out of range. Day-of-week stops at 7.
  • */0 * * * * steps by zero. Nothing matches.
  • 60 * * * * is out of range in the minute field.

The useful habit is to work the next few run times out rather than reading the fields one more time. Writing them down is what makes an OR rule or a step that does not divide 60 obvious. The cron parser on this site breaks an expression into its five fields and reads it back in plain English, which is where a field you misread shows up first.

The expressions that bite are never the complicated ones. They are the ones you already thought you knew.

OCMA Tools

Free developer tools. Most features run client-side, your data stays in your browser. Optional accounts unlock extra features.

Most tools run client-side

© 2026 OCMA Tools — Free developer tools

built for developers, by developers