Cron day-of-week numbering: POSIX vs Quartz vs AWS (the trap that bites everyone moving expressions between systems)
published
Cron expressions look universal — five space-separated fields, asterisks, slashes, ranges. They are not. The day-of-week field alone has at least three numbering conventions in production runners, and the spec disagreements silently break every “copy this cron from Stack Overflow” attempt. This post catalogs the dialects you’ll meet, shows what they actually compile to, and gives the conversion rules.
The expression 0 9 * * 1 means different things
| Runner | Compiled meaning |
|---|---|
POSIX cron (Unix crontab) | Mondays at 09:00 |
cron-parser (Node) | Mondays at 09:00 |
| Kubernetes CronJob | Mondays at 09:00 |
| GitHub Actions | Mondays at 09:00 |
| Cloudflare Workers Cron Triggers | Mondays at 09:00 |
| Quartz (Java) | Sundays at 09:00 |
Spring @Scheduled (Quartz-style) | Sundays at 09:00 |
| AWS EventBridge Scheduler | Sundays at 09:00 |
| Azure Functions (Quartz-style) | Sundays at 09:00 |
Same string, six days apart depending on where you paste it. The cause: POSIX uses 0 = Sunday, 6 = Saturday. Quartz uses 1 = Sunday, 7 = Saturday.
The three dialects
1. POSIX (5-field, 0–6 dow with 0 = Sunday)
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12 or JAN–DEC)
│ │ │ │ ┌───────────── day of week (0–6 or SUN–SAT, 0 = Sunday)
* * * * *
Used by: Unix cron, cron-parser (Node), Kubernetes CronJob, GitHub Actions, Cloudflare Workers, node-cron, agenda. The cron expression builder on this site follows this dialect.
2. Quartz (6 or 7 fields, 1–7 dow with 1 = Sunday)
┌───────────── second (0–59)
│ ┌───────────── minute (0–59)
│ │ ┌───────────── hour (0–23)
│ │ │ ┌───────────── day of month (1–31 or ?)
│ │ │ │ ┌───────────── month (1–12 or JAN–DEC)
│ │ │ │ │ ┌───────────── day of week (1–7 or SUN–SAT, 1 = Sunday)
│ │ │ │ │ │ ┌───────────── year (optional, 1970–2099)
* * * * * * ?
Used by: Quartz Scheduler, Spring @Scheduled (Quartz mode), Azure Functions (NCRONTAB is similar but with seconds optional). The leading seconds field shifts everything right by one.
3. AWS EventBridge (6 fields, 1–7 dow with 1 = Sunday, requires ? on one of dom/dow)
cron(<minutes> <hours> <day-of-month> <month> <day-of-week> <year>)
AWS requires you to use ? (no specific value) in exactly one of day-of-month or day-of-week, never both literal. Quartz-style numbering. Year is required.
cron(0 9 ? * MON-FRI *) # weekdays at 9 AM UTC
cron(0 9 1 * ? *) # 1st of every month at 9 AM UTC
4. @hourly, @daily, @weekly, @monthly, @yearly macros
These are recognized by Unix cron, several Node libraries, and Kubernetes (since 1.4). Translation:
| Macro | 5-field equivalent |
|---|---|
@hourly | 0 * * * * |
@daily, @midnight | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly, @annually | 0 0 1 1 * |
@reboot | runs once at startup (POSIX cron only) |
GitHub Actions does not accept macros. Cloudflare Workers Cron Triggers does not. Many Node libraries accept some but not all. Translate to the explicit 5-field form before deploying.
Conversion rules between POSIX and Quartz
POSIX dow N → Quartz dow (N + 1)
Quartz dow N → POSIX dow ((N - 1) % 7)
Or in plain English: POSIX Sunday is 0, Quartz Sunday is 1. Add 1 going Quartz-ward, subtract 1 going POSIX-ward.
Common pairs:
| Day | POSIX | Quartz / AWS |
|---|---|---|
| Sunday | 0 (or 7) | 1 |
| Monday | 1 | 2 |
| Tuesday | 2 | 3 |
| Wednesday | 3 | 4 |
| Thursday | 4 | 5 |
| Friday | 5 | 6 |
| Saturday | 6 | 7 |
POSIX cron actually accepts both 0 and 7 for Sunday — to ease the porting friction. Quartz does not.
Day-of-month and day-of-week interact differently
POSIX evaluates 0 0 1 * 1 as the 1st of the month OR every Monday — whichever fires. So if the 1st is a Wednesday, the job fires the 1st and the next Monday. The standard semantics is OR when both fields are constrained.
Quartz uses ? as a “no specific value” wildcard. You’re not allowed to constrain both dom and dow simultaneously — use ? on the one you don’t care about.
POSIX: 0 0 1 * 1 → 1st of month OR every Monday
Quartz: 0 0 0 1 * ? → 1st of month, day-of-week irrelevant
Quartz: 0 0 0 ? * MON → every Monday, day-of-month irrelevant
If a Quartz-flavor system rejects your POSIX expression, this is usually why.
Lookup-bench: */N step values
All three dialects use */N for stepping. */15 in the minute field means “every 15 minutes starting at 0”. Caveat: AWS EventBridge documentation occasionally inconsistent — when in doubt, write out the list explicitly: 0,15,30,45.
How DST bites
The cron expression 0 2 * * * (daily at 2 AM) fires:
- Once on the spring-forward day in a DST-aware timezone (02:00 doesn’t exist; the clock jumps 01:59 → 03:00). The job is skipped by most runners.
- Twice on the fall-back day (02:00 happens twice). The job may fire twice depending on the runner.
Defenses:
- Schedule jobs in UTC, not local time. Most cloud runners (Cloudflare Workers, GitHub Actions, Kubernetes CronJob) default to UTC for exactly this reason.
- Avoid the 02:00–02:59 window in DST-observing timezones.
- The cron builder lets you preview fire times in any IANA timezone — use it to inspect what your expression does across a DST boundary.
Quick sanity check before deploying
0 9 * * 1-5 # POSIX/k8s/GHA/CF: weekdays at 09:00
0 0 9 ? * 2-6 * # AWS EventBridge: weekdays at 09:00 UTC (Mon=2, Fri=6)
0 0 9 ? * MON-FRI * # AWS EventBridge: same, named days
Compile-time difference: 4 fields → 6 fields, dow shift +1, year required, ? on dom.
Tooling
The cron expression builder and tester on this site uses the POSIX dialect (0 = Sunday). It describes any 5-field expression in plain English and previews the next 10 fire times in your chosen timezone — for sanity-checking before you paste into crontab, a Kubernetes manifest, a GitHub Actions workflow, or any other POSIX-compatible runner.
For Quartz or AWS EventBridge expressions, do the numeric translation first, then verify with the runner’s own documentation. The translation rule is just ±1 on the day-of-week field — most other things are the same.