Cron syntax across Linux, Kubernetes, GitHub Actions, AWS and Quartz
The five-field expression you know shows up all over modern infrastructure — with subtle, breaking differences. A field-by-field comparison across the systems you'll actually meet.
The five-field cron expression escaped Unix decades ago. Today you'll meet it in container orchestrators, CI pipelines and cloud schedulers — each with its own subtle twist. Copying an expression from one to another without checking is a classic way to get a job that runs at the wrong time, or not at all.
Standard Unix cron
The baseline. Five fields — minute, hour, day-of-month, month, day-of-week — plus the @daily-style macros. Runs in the server's local timezone. Everything below is a variation on this.
0 9 * * 1-5 → 9 AM on weekdays
Kubernetes CronJob
Kubernetes uses standard 5-field cron verbatim, so your expressions carry over directly. The differences are around it, not in it:
spec.scheduleholds the cron string.spec.timeZone(stable since Kubernetes 1.27) lets you pin a timezone instead of relying on the controller's — a welcome fix for the timezone trap.concurrencyPolicy(Allow/Forbid/Replace) controls what happens when a run is still going and the next is due — the built-in answer to overlapping jobs.startingDeadlineSecondsdecides whether a missed run (e.g. the controller was down) still fires.
So the expression is standard; the behaviour around missed and overlapping runs is configurable in a way plain cron never was.
GitHub Actions
GitHub Actions schedules workflows with 5-field cron, but with hard constraints:
on:
schedule:
- cron: "0 9 * * 1-5"
- UTC only. There is no timezone option —
0 9 * * *is 9 AM UTC, full stop. Do the offset in your head. - The shortest interval is 5 minutes; anything more frequent is rejected or coalesced.
- Scheduled runs are best-effort and can be delayed during high load (sometimes by many minutes), so don't rely on exact timing.
- Macros like
@dailyare not supported — use the five fields.
AWS EventBridge / CloudWatch
AWS Scheduler and EventBridge use a 6-field cron() expression — the extra field is year — and they change two symbols:
cron(0 9 ? * MON-FRI *) → 9 AM weekdays
- Six fields: minute, hour, day-of-month, month, day-of-week, year.
- You must use
?in either day-of-month or day-of-week (you can't specify both as*), because AWS forbids the ambiguous combination. - Day-of-week is 1–7 with Sunday = 1, not 0 — off by one from Unix.
- There's also a simpler
rate(5 minutes)form for fixed intervals.
This is the dialect most likely to trip you up: an expression copied from a Linux crontab will usually be rejected outright.
Quartz (Java) and Spring @Scheduled
Quartz — and Spring's @Scheduled(cron = "…"), which uses Quartz-style syntax — expects 6 or 7 fields, leading with seconds:
0 0 9 ? * MON-FRI → 9 AM weekdays
- Fields: seconds, minutes, hours, day-of-month, month, day-of-week, and an optional year.
- Like AWS, it uses
?for "no specific value" in the day fields. - It adds special characters plain cron lacks:
L(last),W(nearest weekday) and#(nth weekday of the month) — so "the first Monday of the month" (? * 2#1) is finally expressible. - Day-of-week is 1–7 with Sunday = 1.
The quick comparison
| System | Fields | Seconds? | Timezone | Day-of-week |
|---|---|---|---|---|
| Unix cron | 5 | No | Server local | 0–6, Sun = 0 |
| Kubernetes | 5 | No | Configurable | 0–6, Sun = 0 |
| GitHub Actions | 5 | No | UTC only | 0–6, Sun = 0 |
| AWS EventBridge | 6 (+year) | No | UTC / configurable | 1–7, Sun = 1 |
| Quartz / Spring | 6–7 | Yes | Configurable | 1–7, Sun = 1 |
The takeaway
"Cron syntax" is really a family of closely-related dialects. Before you move an expression between systems, check three things: how many fields it expects, whether Sunday is 0 or 1, and which timezone it runs in. When in doubt, decode the plain 5-field version in the parser first, then translate it deliberately into the target system's dialect.