Skip to content
DDevToolery

19 March 2025 · 5 min read

The cron field that runs your job more often than you meant

When both day-of-month and day-of-week are set, POSIX cron uses OR, not AND.

Standard cron has five fields: minute, hour, day of month, month, day of week. Four of them behave the way you expect. The last two, together, do not.

The rule

If both day-of-month and day-of-week are restricted — neither is an asterisk — cron runs the job when either matches, not when both do.

0 0 1 * 1    # intended: midnight on the 1st, if it is a Monday
             # actual:   midnight on the 1st, AND every Monday

A job you expected to run about twice a year runs roughly fifty-three times. The expression is valid, the syntax is correct, and nothing warns you.

The behaviour is specified, not a bug. POSIX defines it explicitly. It is simply the opposite of what almost everyone assumes on first reading.

Why it works that way

The two fields describe the same thing — which day — through different calendars. Historically, the union was the more useful default: it lets a single line express something like the 1st of the month and every Monday, which AND semantics cannot express at all.

Getting AND behaviour

You cannot, in standard cron. The usual approach is to schedule on one field and test the other in the job itself:

# Run at midnight on the 1st, but only act if it is a Monday.
0 0 1 * *  [ "$(date +\%u)" = "1" ] && /usr/local/bin/monthly-report

Every scheduler is different

  • Quartz adds a seconds field at the front, so an expression that works in cron is off by one field.
  • Quartz and AWS EventBridge require a ? in one of the two day fields precisely to avoid this ambiguity, and reject an expression that restricts both.
  • AWS EventBridge also puts the year at the end, making six fields.
  • systemd timers use OnCalendar with an entirely different syntax and no cron compatibility.
  • Kubernetes CronJobs follow standard five-field cron, including the OR behaviour.

Timezones

Classic cron runs in the system timezone, so a daily 02:30 job is skipped on the spring transition and runs twice on the autumn one. If a job must run exactly once daily, schedule it in UTC or pick a time outside the transition window. Kubernetes CronJobs default to UTC; EventBridge is always UTC.

Before you deploy it

Read the expression back in plain English and check the next several fire times against what you intended. Most cron mistakes are not typos — they are correct expressions that mean something other than what the author had in mind.

Tools mentioned