Cron syntax, actually explained
The five standard cron fields, the Quartz six and seven field variants, macros, step values, the day-of-month versus day-of-week OR rule, and the timezone traps.
Ben Ennis
Published July 27, 2026
Cron syntax is small enough to learn in ten minutes and subtle enough that experienced engineers still get the day fields wrong. There are also at least three dialects in common use — POSIX/Vixie cron, Quartz, and the Kubernetes CronJob flavour — and they disagree about field counts, allowed characters, and what day-of-week numbering means. This is the reference for all three.
The five standard fields
┌──────────── minute (0-59)
│ ┌────────── hour (0-23)
│ │ ┌──────── day of month (1-31)
│ │ │ ┌────── month (1-12 or JAN-DEC)
│ │ │ │ ┌──── day of week (0-7 or SUN-SAT; 0 and 7 both mean Sunday)
│ │ │ │ │
* * * * * command
The canonical description of the five fields and their ranges is the crontab(5) manual page, and the interface itself is specified in POSIX crontab. Read the manual page for your actual cron implementation when precision matters: Vixie cron, cronie, and busybox crond all differ around the edges.
Worked examples:
0 3 * * * 03:00 every day
*/15 * * * * every 15 minutes, on :00 :15 :30 :45
0 9-17 * * 1-5 hourly from 09:00 to 17:00, Monday to Friday
30 2 1 * * 02:30 on the first of every month
0 0 * * 0 midnight on Sunday
15 14 1 * * 14:15 on the first of every month
The operators
Four operators, and they compose within a single field:
*— every value in the field.,— a list:0,15,30,45.-— an inclusive range:9-17./— a step, applied to a range:0-23/2is every second hour;*/15is shorthand for0-59/15in the minute field.
Steps are the operator people misread. */7 in the minute field does not mean
“every seven minutes” in the way you might hope — it means minutes 0, 7, 14, 21,
28, 35, 42, 49, and 56, and then the hour rolls over and the next fire is at
minute 0, four minutes later. Any step that does not divide the field range
evenly produces an uneven gap at the boundary. Use divisors of 60 for minutes
and of 24 for hours if you want a regular interval.
Names work in the month and day-of-week fields — JAN-DEC and SUN-SAT —
but they are case-insensitive three-letter abbreviations only, and you cannot use
a step with a name range in every implementation. Numbers are more portable.
The day-of-month and day-of-week rule
This is the one genuine trap in standard cron. Normally, a job runs when all
fields match. But if both the day-of-month and day-of-week fields are
restricted — meaning neither is * — the job runs when either matches.
The crontab(5) manual page states it directly and gives this example:
30 4 1,15 * 5 04:30 on the 1st and 15th of each month, PLUS every Friday
That is almost certainly not what someone writing it intended. If you want “the first Monday of the month”, standard cron cannot express it; the usual workaround is to schedule every Monday and exit early in the script when the day of month exceeds 7:
# 0 6 * * 1 — runs every Monday, does work only on the first one
[ "$(date +%-d)" -le 7 ] || exit 0
Quartz can express it directly, which is one of the few good reasons to prefer Quartz syntax.
Macros
Most implementations accept @-prefixed shorthands. The
crontab(5) page lists
@reboot, @yearly (and its synonym @annually), @monthly, @weekly,
@daily, and @hourly.
@yearly 0 0 1 1 *
@monthly 0 0 1 * *
@weekly 0 0 * * 0
@daily 0 0 * * *
@hourly 0 * * * *
@reboot once, when the cron daemon starts
@reboot is the odd one out: it is not a schedule, it is an event, and it does
not exist in Kubernetes CronJobs or Quartz. Kubernetes additionally accepts
@midnight as a synonym for @daily, per the
CronJob documentation.
Be wary of @daily and @hourly in a fleet: every host runs them at exactly
:00, which is how you get a thundering herd against a shared database at
midnight. Spread the load with an explicit random-looking minute instead.
Quartz: six or seven fields
Quartz is the Java scheduler dialect, and it is what you will meet in Spring applications, Jenkins-adjacent tooling, and several commercial schedulers. It uses six mandatory fields plus an optional seventh:
seconds 0-59
minutes 0-59
hours 0-23
day of month 1-31
month 1-12 or JAN-DEC
day of week 1-7 or SUN-SAT (1 = Sunday, unlike standard cron)
year 1970-2099 (optional)
Two differences will break a copied expression immediately. Quartz has a seconds
field first, so a standard five-field expression pasted into Quartz is
interpreted one field to the left. And Quartz numbers day-of-week from 1, where
standard cron numbers from 0 — so 5 means Friday in cron and Thursday in
Quartz.
Quartz also adds four special characters that standard cron does not have:
?— “no specific value”, used in one day field when the other is set. Quartz requires exactly one of the two day fields to be?.L— last.Lin day-of-month is the last day of the month;6Lin day-of-week is the last Friday.W— nearest weekday.15Wfires on the weekday closest to the 15th.#— the nth weekday.6#3is the third Friday of the month.
0 0 3 ? * MON-FRI 03:00:00 on weekdays
0 0 12 L * ? noon on the last day of every month
0 15 10 ? * 6#3 10:15 on the third Friday
The formal grammar is in the
CronExpression API documentation.
Note that Kubernetes accepts ? as a synonym for * but supports none of L,
W, or #, so a Quartz expression will often parse in a CronJob and mean
something different.
Timezones, which is where the real incidents come from
Standard cron runs in the local timezone of the machine, which means a job scheduled for 02:30 either runs twice or not at all on daylight-saving transition days. The only reliable fix is to run schedulers in UTC and convert at the edges.
Kubernetes made this explicit: .spec.timeZone on a CronJob accepts an IANA
timezone name and has been stable since v1.27, per the
CronJob documentation.
Setting CRON_TZ or TZ inside the schedule string is not supported and will
be rejected. Without the field, the schedule is interpreted in the timezone of
the kube-controller-manager, which is a property of your control plane rather
than of your manifest — a bad thing to depend on.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 3 * * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: reports:1.4.2
Kubernetes CronJob behaviour worth knowing
Three documented behaviours account for most CronJob surprises, all described on the CronJob concept page and in the CronJob v1 API reference:
The controller polls every ten seconds. A startingDeadlineSeconds under
10 may cause the job never to be scheduled at all.
More than 100 missed schedules stops scheduling. If the controller cannot
reach a scheduled time — control plane restart, clock skew, a long outage — it
counts how many schedules it missed since the last run. Past 100, it gives up
and logs too many missed start times. Setting startingDeadlineSeconds
bounds the window it examines, which is the fix.
Execution is at-least-once, not exactly-once. The documentation states plainly that two Jobs may be created for one scheduled time, or none. Idempotent job bodies are not optional.
concurrencyPolicy is Allow by default. For anything that touches shared
state, set it to Forbid (skip the new run) or Replace (kill the old one) and
decide which failure you prefer.
When cron is the wrong tool
Cron is a time trigger with no state, no retries, no dependency graph, and no
history. If you need any of those, use something else. On a single Linux host,
systemd timers
give you Persistent=true for catch-up after downtime, RandomizedDelaySec
for herd avoidance, and journal-integrated logs, using the calendar syntax
documented in
systemd.time(7).
In Java, Spring’s scheduling support is documented in the
Spring Framework reference.
On AWS, EventBridge uses its own six-field variant described in the
EventBridge cron documentation
— another dialect, with its own day-of-week numbering.
And if the job’s real trigger is an event rather than a clock, cron is a polling workaround. Schedule on the event.
To check an expression before you ship it, the cron expression builder on this site parses both standard and Quartz syntax, describes it in English, and shows the next ten fire times in the timezone you choose.
Frequently asked questions
Why does my job with both day fields set run more often than expected?+
*, the job runs if either matches. 30 4 1,15 * 5 runs on the 1st, the 15th, and every Friday — see crontab(5).Does */7 in the minute field mean every seven minutes?+
How do I set a timezone for a Kubernetes CronJob?+
.spec.timeZone field with an IANA name such as Europe/London. It has been stable since Kubernetes v1.27. Setting CRON_TZ inside the schedule string is not supported.Can I paste a Quartz expression into a Kubernetes CronJob?+
? as a synonym for * but not L, W, or #.Why did my CronJob stop scheduling entirely?+
too many missed start times. Set startingDeadlineSeconds so the controller only considers a bounded window.Read next
- kubectl cheatsheet (2026 edition)
- Try the tool: Cron expression builder
- Try the tool: Timestamp converter
Tags: #cron, #kubernetes, #scheduling, #operations