Cron Expressions and Common Pitfalls
Cron expressions schedule recurring jobs using compact space-separated fields, but their syntax has dialect splits and a notorious day-of-month / day-of-week OR-quirk that has caused outages for decades.
A cron expression is a compact schedule string parsed by a scheduler daemon or library. The classic Unix cron dialect popularised by Vixie Cron uses five space-separated fields read left to right: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12 or JAN-DEC), and day-of-week (0-6, where 0 is Sunday; 7 is also accepted as Sunday by Vixie cron). Each field accepts wildcards (`*`), lists (`1,15,30`), ranges (`9-17`), and step values (`*/5`). Most Unix implementations also expose `@`-prefixed nicknames such as `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly` (or `@annually`), and the special `@reboot`, which fires once when the cron daemon starts. Other dialects extend the grammar. Quartz Scheduler (Java) uses six required fields, prepending seconds and shifting day-of-week to 1-7 with Sunday=1, plus an optional seventh year field and extras like `L` (last), `W` (nearest weekday), `#` (nth weekday of month), and the `?` placeholder required in exactly one of the two day fields. AWS EventBridge rules use a six-field form with year, always evaluated in UTC. GitHub Actions `schedule` keeps the standard five fields, also UTC-only, and enforces a five-minute minimum granularity. The most consequential pitfall is the day-of-month vs day-of-week interaction. In Vixie cron and its descendants, if **both** day fields are restricted (neither is `*`), the job fires when **either** matches — an OR, not an AND. So `0 0 13 * 5` does not mean Friday the 13th; it fires every 13th and every Friday, roughly nine times a month. Quartz and a few others use AND semantics, which is why Quartz forces a `?` in one of the two day fields to disambiguate. Daylight Saving Time introduces another class of bugs. On the spring-forward boundary, jobs scheduled in the skipped hour never run on plain POSIX cron; Debian's patched Vixie reschedules them shortly after the jump for fixed-time jobs but not for wildcard minute/hour entries. On fall-back, the repeated hour can fire jobs twice unless the daemon tracks the offset. Schedulers that store expressions with an explicit timezone (Quartz, Anacron-adjacent systemd timers via `OnCalendar`) sidestep most of this. Other recurring confusions: `*/5` in the minute field steps from minute zero, not from when the job is installed; `0 0 1 * *` is monthly while `0 0 * * 0` is weekly on Sunday; midnight is `0 0` (00:00 of the new day), never `24 0`; POSIX requires numeric months and weekdays, while Vixie accepts JAN-DEC and SUN-SAT case-insensitively. When in doubt, test the expression in a parser that names the matching dialect, because the same string can mean different things to different daemons.