The 20 cron expressions everyone copies, explained
Updated 2026-08-09
A cron expression is five fields — minute, hour, day of month, month, day of week — and every field is a match condition, not an interval. `*/15 * * * *` does not mean "every 15 minutes starting now"; it means "any minute whose value is divisible by 15", which is why it fires at :00, :15, :30, and :45 no matter when you installed the job. Most cron surprises trace back to that one distinction: cron matches wall-clock field values, it never counts elapsed time.
The table below covers the 20 expressions that show up in practically every crontab, CI config, and deployment YAML, with the trap attached to each. You can paste any row into the cron expression parser to see the field breakdown and the next five runs in your own timezone.
How to read the five fields
minute (0-59) | hour (0-23) | | day of month (1-31) | | | month (1-12) | | | | day of week (0-7; both 0 and 7 are Sunday) | | | | | * * * * *
Each field accepts a single value (`5`), a list (`1,15`), a range (`8-18`), a step (`*/10` or `8-18/2`), or `*` for "any". Some schedulers — Quartz, Spring's @Scheduled, several cloud cron services — prepend a seconds field, making six fields total. If an expression you copied has six fields and your crontab expects five, the whole schedule shifts one field to the left and silently means something completely different, so count fields before you paste.
That list of accepted forms is exactly what the parser behind the cron tool here implements: 5-field and 6-field (leading seconds) expressions, and within each field `*`, single values, lists, ranges, and steps in both `*/n` and `a-b/n` form. Day-of-week accepts 0-7 with 7 normalized to Sunday, and the Vixie OR rule applies when both day fields are restricted. It renders a plain-English breakdown per field and computes the next five runs in your browser's local timezone.
What it deliberately does not do: month and weekday names (`JAN`, `MON`) are rejected — fields are numeric only. Typed macros like `@daily` or `@reboot` are not parsed; the preset buttons insert the numeric equivalents instead. Quartz extensions (`?`, `L`, `W`, `#`) are unsupported. And while a 6-field expression validates and gets a seconds-field description, the next-run preview is computed at minute granularity, so sub-minute schedules show minute-level times. If you need `L` ("last day of month") or named fields, verify against the scheduler that will actually run the job.
The 20 expressions
| Expression | Fires | The catch |
|---|---|---|
| * * * * * | Every minute | Overlapping runs if the job takes longer than 60s; make it idempotent or use a lockfile |
| */5 * * * * | Every 5 minutes, at :00, :05, :10... | Aligned to the hour, not to when the job was installed |
| */10 * * * * | Every 10 minutes | Same alignment rule |
| */15 * * * * | At :00, :15, :30, :45 | Verified: from 10:07 the next run is 10:15, not 10:22 |
| */30 * * * * | At :00 and :30 | Not "twice an hour at arbitrary times" |
| 0 * * * * | Top of every hour | Everyone schedules :00; pick a spare minute like 7 to dodge the thundering herd |
| 0 */2 * * * | Even hours: 00:00, 02:00, ... | Steps count from 0, so it is always even hours |
| 0 0 * * * | Daily at midnight | Midnight in the daemon's timezone, not yours; equivalent to @daily |
| 0 9 * * * | Daily at 09:00 | DST shifts what 09:00 means relative to UTC twice a year |
| 30 4 * * * | Daily at 04:30 | A common "quiet window" pick; safer than 02:30 in DST zones |
| 0 9 * * 1-5 | Weekdays at 09:00 | 1-5 is Mon-Fri in standard cron; in Quartz the same range means Sun-Thu |
| 0 0 * * 0 | Sunday at midnight | Equivalent to @weekly |
| 0 0 * * 1 | Monday at midnight | The usual "start of week" job |
| 0 0 * * 6,0 | Saturday and Sunday at midnight | Weekend list has to wrap through 0; 6-7 also works where 7 is accepted |
| 0 0 1 * * | First of the month at midnight | Equivalent to @monthly |
| 0 0 1,15 * * | 1st and 15th at midnight | The classic payroll/reporting pair |
| 0 0 31 * * | The 31st at midnight | Only 7 months have a 31st; verified next runs skip February and April entirely |
| 0 8-18 * * * | Hourly, 08:00 through 18:00 | The range is inclusive: that is 11 runs a day, not 10 |
| 0 0 1 1 * | January 1 at midnight | Equivalent to @yearly / @annually |
| 0 12 13 * 5 | Intended: Friday the 13th at noon | Actually the 13th OR any Friday — see the OR rule below |
*/N is alignment, not an interval
A step expression expands to the set of field values reachable from the range start: `*/15` in the minute field is exactly `0,15,30,45`. The scheduler never remembers when the last run happened. This is harmless when N divides 60 evenly, and quietly wrong when it does not. `*/40 * * * *` looks like "every 40 minutes" but expands to minutes `0,40` — verified next runs from 10:00 are 10:40, 11:00, 11:40: a 40-minute gap, then a 20-minute gap, forever.
The same reset happens at month boundaries. `0 0 */2 * *` ("every other day") expands day-of-month to 1,3,5,...,31, and the sequence restarts on the 1st of every month. Verified: it fires on July 31 and then again on August 1 — two consecutive days. If you need a true fixed interval, cron is the wrong tool; use a systemd timer with OnUnitActiveSec or an interval-based scheduler. For awkward periods that do divide the day, two crontab lines work: `0 0-22/3 * * *` plus `30 1-23/3 * * *` together fire every 90 minutes (verified: 00:00, 01:30, 03:00, 04:30, ...).
Day-of-month and day-of-week combine with OR, not AND
This is the most damaging cron gotcha because it looks so reasonable. When both the day-of-month field and the day-of-week field are restricted (neither is `*`), classic Vixie cron runs the job when either one matches. `0 12 13 * 5` reads like "noon on Friday the 13th" but actually fires at noon on every 13th and at noon on every Friday. Verified with croniter from 2026-08-09: the next five runs are Thu Aug 13, Fri Aug 14, Fri Aug 21, Fri Aug 28, Fri Sep 4 — one "13th" match and four plain Fridays.
If either of the two fields is `*`, the other constrains alone, which is why `0 9 * * 1-5` behaves as expected. To get a real AND, move one condition into the command:
0 12 13 * * [ "$(date +\%u)" = 5 ] && /usr/local/bin/spooky-job # note: % must be escaped as \% inside a crontab line
This site's cron parser implements the same OR rule as Vixie cron when both fields are restricted, so its next-run preview will show you this behavior before it shows up in production.
Sunday is 0 and 7 — unless you are on Quartz
Standard cron accepts both 0 and 7 as Sunday in the day-of-week field. Verified: `0 0 * * 7` produces the same Sunday-midnight schedule as `0 0 * * 0`. Some stricter parsers reject 7, so 0 is the portable choice. The real hazard is Quartz (and therefore a lot of Java scheduling), where day-of-week runs 1-7 meaning Sunday through Saturday. A `1-5` range copied from a crontab into a Quartz trigger changes from Mon-Fri to Sun-Thu — the job starts running on Sundays and silently skips Fridays. The parser on this site accepts 0-7 and normalizes 7 to 0, matching classic cron.
Midnight jobs, DST, and the timezone cron actually uses
`0 0 * * *` means midnight in the timezone of whatever evaluates the expression — the cron daemon's system timezone, the CI runner's zone, or the CronJob controller's zone — not the timezone of whoever wrote it. On top of that, daylight saving makes some wall-clock times not exist, or exist twice. Verified with croniter in America/New_York: on 2026-03-08 (spring forward) a `30 2 * * *` job resolves to 03:00 EDT because 02:30 never occurs; on 2026-11-01 (fall back) a `30 1 * * *` job matches twice — 01:30 EDT and 01:30 EST. Different daemons handle these edges differently, which is exactly why you should not be scheduling into them.
- Run infrastructure jobs in UTC and convert in your head once, not in every config.
- If the box must use local time, schedule between 03:00 and 04:00 in US zones — that hour always exists exactly once.
- Expect a daily UTC job to drift by an hour twice a year relative to local business hours; if the business deadline is local, the schedule must be local (recent Kubernetes CronJobs support a timeZone field for exactly this).
- When converting a timestamp in logs to figure out why a run fired at a weird hour, the timestamp converter on this site shows the same epoch in UTC and your local zone side by side.
Questions people ask
Is @daily the same as 0 0 * * *?
Yes — @daily and @midnight are both aliases for 0 0 * * * in cron implementations that support macros, meaning midnight in the daemon's timezone. Not every scheduler accepts the @ macros, so the numeric form is the portable one.
How do I run a job on the last day of every month?
Standard cron has no L token (that is a Quartz extension). The usual workaround is to run on days 28-31 and let the command check whether tomorrow is the 1st: 0 23 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && your-job (GNU date syntax).
Can a single cron expression run every 90 minutes?
No. Steps expand within one field, and no combination of minute and hour steps yields a uniform 90-minute period. Use two entries — 0 0-22/3 * * * and 30 1-23/3 * * * — which interleave to fire exactly every 90 minutes (verified), or use an interval-based scheduler.
Why did my 02:30 job not run last night?
If last night was a spring-forward DST transition, 02:30 never existed on the clock. Verified behavior differs by implementation — croniter shifts the run to 03:00 — but the reliable fix is scheduling in UTC or inside the 03:00-04:00 window.