Every cron expression is five tiny filters wearing a trench coat. Once you see the fields — minute, hour, day of month, month, day of week — strings like 30 4 1,15 * * stop being intimidating and start being almost boring: 4:30 AM on the 1st and 15th. This guide builds the reading skill from scratch, covers the Quartz extras you'll meet in Java schedulers and cloud consoles, and shows how to verify a schedule by looking at when it actually fires next.
Read left to right, smallest unit to biggest:
| Position | Field | Values | Names allowed |
|---|---|---|---|
| 1 | Minute | 0–59 | no |
| 2 | Hour | 0–23 | no |
| 3 | Day of month | 1–31 | no |
| 4 | Month | 1–12 | JAN–DEC |
| 5 | Day of week | 0–6 (0 = Sunday) | SUN–SAT |
The rule that makes everything click: a job runs at every minute where all five fields match at the same time. An asterisk is a wildcard that matches everything, so * * * * * is "every minute of every day." Change only the first field to */15 and you get every fifteen minutes. The four *s didn't change — you just tightened one filter.
Each field accepts four building blocks, and you can combine them:
*/15 — every 15th value starting at the field minimum: 0, 15, 30, 45. Works inside ranges too: 9-17/2 is 9, 11, 13, 15, 17.9-17 — inclusive on both ends. Hours 9 through 17 means the job can fire any time from 9:00 up to 17:59, which is why "between 9 and 5" schedules actually run during the 5 PM hour.1,15 — exactly the values listed, no padding.30 — that one value.A classic business-hours check, */15 9-17 * * 1-5, reads: every 15 minutes, during the hours 9:00–17:59, any day of the month, any month, Monday through Friday. Five fields, five filters, one sentence.
Historical accident worth 30 seconds: original Unix cron numbered Sunday 0. ISO 8601 later standardized Monday 1 through Sunday 7, and to keep both camps happy, most implementations accept 0 and 7 as Sunday. Practical consequence: 1-5 is weekdays in both systems, but 6-7 vs 0-6 style weekend ranges should be written 6,0 or SAT,SUN to be unambiguous everywhere. When in doubt, use the names — every modern parser understands MON-FRI.
These are Quartz additions (also supported by many cloud schedulers) for the things five plain fields can't say:
| Symbol | Field | Means | Example |
|---|---|---|---|
L | day of month | last day of the month | 0 12 L * * — noon on the 28th/29th/30th/31st |
nL | day of week | last given weekday of the month | 6L — last Friday |
W | day of month | weekday nearest that date | 15W — the 15th, or the closest weekday |
n#m | day of week | the m-th weekday n of the month | 2#1 — first Tuesday (patch Tuesday) |
? | day fields | no specific value | required by Quartz when the other day field is set |
The ? rule trips people up in Quartz specifically: you may not restrict day-of-month and day-of-week at once, so whenever one of them names real days, the other becomes ?. Standard five-field cron has no such rule — there, when both day fields are restricted, many implementations fire on either match (OR), not both. That OR is the single most surprising cron behavior, and it's why 0 0 13 * 5 means "the 13th, plus every Friday" on a classic crontab, not "Friday the 13th."
Nicknames for schedules everyone writes constantly:
| Nickname | Expands to | Runs at |
|---|---|---|
@hourly | 0 * * * * | top of every hour |
@daily / @midnight | 0 0 * * * | midnight every day |
@weekly | 0 0 * * 0 | midnight Sunday |
@monthly | 0 0 1 * * | midnight on the 1st |
@yearly / @annually | 0 0 1 1 * | midnight January 1st |
Two usual suspects. Timezones: crontab evaluates in the server's local time by default, so a job set for 9:00 on a UTC box fires at 9:00 UTC — 5 AM in New York. Cloud schedulers let you pin an IANA zone, which is always worth doing for anything user-facing. DST: when clocks spring forward, the 2:00–3:00 AM hour doesn't exist and a job scheduled inside it either runs late or is skipped entirely; when clocks fall back, a job in the repeated hour can fire twice. Schedules at :00 past odd hours (like 3:15) sidestep both edges.
The reliable habit: before trusting any expression, look at its next few actual fire times — not the fields, the dates. If 0 9 * * MON-FRI shows you Tue 09:00, Wed 09:00, Thu 09:00, Fri 09:00, then Mon 09:00, you can see the weekend skip with your own eyes instead of reasoning about it.
Paste an expression, get the English sentence plus its next five run times in your timezone.
Open the Cron Explainer0 9 * * MON-FRI — 9:00 AM, Monday through Friday: the daily standup report.30 4 1,15 * * — 4:30 AM on the 1st and 15th: semi-monthly billing.*/5 8-18 * * 1-5 — every 5 minutes from 8:00 to 6:59 PM, weekdays: a business-hours monitor.0 12 L * * — noon on the last day of each month: month-end rollup, no calendar hard-coding.0 15 10 ? * 6L — 10:15 on the last Friday of every month (Quartz): the release-window classic.If you got all five without peeking, you can read cron. When you need to write one instead, a field-by-field cron generator is the safer route — compose it, then paste the result into the explainer to confirm it says what you meant. And when the schedule itself isn't the mystery but "when did it last run" is, convert fire times with an epoch converter to see them alongside your logs.