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.

Published 2026-09-25

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. Every date +%u in a crontab is date +\%u.
  • Keep the OR in mind when choosing which field to restrict. 0 0 * * 5 plus a date +\%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.