The cron dom/dow Trap — Why Two Day Fields OR Instead of AND
Standard cron ORs day-of-month and day-of-week when both are set — 0 0 13 * 5 fires every Friday AND every 13th. How to get AND instead.
Here is the single most expensive cron surprise, ranked by the number of production incidents it has caused:
0 0 13 * 5 /usr/local/bin/full-backup
Read it the way most people do — “midnight on Friday the 13th” — and you’ve just scheduled a heavy backup for every Friday at 00:00, plus the 13th of every month. In a bad month that’s six runs instead of one.
The actual rule
POSIX says it plainly: the minute, hour and month fields must all match, and at least one of the two day fields must match — where “must match” is shorthand that flips meaning depending on whether the field is *:
- day-of-month
*, day-of-week restricted → the weekday decides (the familiar case) - day-of-month restricted, day-of-week
*→ the month-day decides - both restricted → the job runs when either matches — a logical OR
So 0 0 13 * 5 expands to “midnight, whenever it’s the 13th or a Friday”. The two fields don’t narrow each other — they widen the schedule. Paste 0 0 13 * 5 into the calculator above and the warning chip calls this out; the next-runs list shows Fridays and 13ths interleaved.
Why would anyone design it that way?
Because OR is what makes the day fields expressive. Without it, “run on the 1st of the month and every Monday” (0 0 1 * 1 — a real schedule people want for billing reminders) would be unwritable: there’d be no syntax for “either of these day conditions”. AND-only fields can express intersection but never union. POSIX picked union, and every crontab since has inherited it — the surprise isn’t the design, it’s that the syntax looks like intersection.
The dialects genuinely disagree
This is one of the places where “cron” stops meaning one thing:
| engine | both day fields set | notes |
|---|---|---|
| Vixie/ISC cron, cronie (Linux, BSD, macOS) | OR | POSIX behavior |
GitHub Actions schedule: |
OR | same 5-field dialect |
| Quartz (Spring, Java EE, AWS EventBridge) | AND | one field must be ? |
| Kubernetes CronJob | OR | standard cron, no ?/L/W/# |
Same five fields, opposite semantics. An expression copied from a Quartz config into a crontab doesn’t just risk a syntax error on ? — where it parses, it can schedule a job with completely different timing. When a schedule “can’t be right” on one platform, check which dialect the config file speaks before blaming cron.
How to get AND when you want it
Cron gives you union; intersection you build yourself. The idiom: restrict one day field, leave the other *, and test the remaining condition in the command:
# Friday the 13th at 00:00 — cron fires every 13th, the guard keeps Fridays
0 0 13 * * [ "$(date +\%u)" -eq 5 ] && /usr/local/bin/friday-13-job
# 1st of the month AND a Monday (first-Monday reporting run)
0 0 1 * * [ "$(date +\%u)" -eq 1 ] && make monthly-report
# last Friday of the month — fires every Friday, guard keeps the last one
0 2 * * 5 [ "$(date -d '+7 days' +\%m)" != "$(date +\%m)" ] && month-end-close
Two details that make or break this pattern:
%must be escaped as\%inside crontab. Unescaped%becomes a newline in the command — the classic silent-failure covered in debugging cron. Everydate +%uin a crontab isdate +\%u.- Keep the OR in mind when choosing which field to restrict.
0 0 * * 5plus adate +\%d-le-07 guard is another way to say “first Friday” — pick whichever side reads clearer to the next person; cron doesn’t care, the guard does the intersecting.
Test it before it mails you at midnight
The verification habit that catches this whole class of bug: after writing a day-restricted line, paste it into a next-run preview (the calculator above) and check the first few dates — 0 0 13 * 5 will list Jan 2 (a Friday) and Jan 9 before it ever lists a 13th, which makes the OR unmistakable. If the preview shows Fridays you didn’t mean, AND it out with a guard instead.
Frequently asked questions
So 0 0 13 * 5 really runs every Friday?
Yes — and on every 13th. In standard cron, when day-of-month and day-of-week are both restricted (neither is *), the job fires when either field matches. It runs roughly 5–6 times a month, not on the rare Friday the 13th.
How do I run something only on Friday the 13th?
Restrict one day field and test the other in the command: 0 0 13 * * [ "$(date +\%u)" -eq 5 ] && cmd — the line fires every 13th, and the guard passes only on Fridays. Remember the % must be escaped as \% inside crontab, or cron turns it into a newline.
Does this OR rule apply everywhere?
No — it's POSIX/Vixie-cron behavior (Linux, BSD, macOS crontab, GitHub Actions). Quartz-derived schedulers — AWS EventBridge, Spring, Oracle's — AND the two fields instead, and force you to write ? in the one you don't care about. Same characters, opposite meaning. Always check which engine reads the line.
What about 0 0 * * 5 — is that still every Friday?
Yes. When one day field is *, the other alone decides — there's nothing to OR. The rule only changes behavior when both are restricted.
Isn't this just a bug everyone lives with?
It's deliberate, standardized behavior — POSIX specifies the OR so you can write "on the 1st and on Mondays" schedules like 0 0 1 * 1. The bug is in the intuition that two restrictive-looking fields should narrow each other. They widen.