Cron Expression Syntax, Timezone Drift, and Overlapping Job Pitfalls
Five space-separated fields govern scheduled workloads across Linux crontab, Kubernetes CronJob, GitHub Actions, and cloud event buses. While basic schedules look simple, subtle rules in the POSIX cron specification regularly trigger duplicate runs, uneven intervals, and skipped jobs in production.
1. The 5-Field POSIX Layout
Standard Vixie cron evaluates five fields in fixed order: minute, hour, day of month, month, and day of week. You can test any expression interactively and inspect upcoming execution times in the Cron Expression Parser.
| Field | Allowed Values | Special Operators | Notes |
|---|---|---|---|
| Minute | 0-59 | * , - / | Zero-indexed minute of the hour |
| Hour | 0-23 | * , - / | 24-hour clock (0 is midnight, 12 is noon) |
| Day of Month (DOM) | 1-31 | * , - / | Months with fewer days silently skip out-of-range dates (e.g. Feb 30) |
| Month | 1-12 | * , - / | 1 = January through 12 = December |
| Day of Week (DOW) | 0-6 (or 7) | * , - / | 0 is Sunday in standard POSIX cron (some parsers also accept 7 as Sunday) |
2. Trap #1: The Step Operator (*/N) Is Modulo, Not an Interval Timer
Developers often write */23 * * * * expecting a job to run every 23 minutes continuously. In cron, */N is a shorthand for matching field values where (value - min) % N === 0 within the field's reset boundary.
- In the minute field (
0-59),*/23fires at minutes0,23, and46of each hour. - After minute
46, the next execution happens at minute0of the next hour: a gap of 14 minutes instead of 23. - Only divisors of 60 (
1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30) produce uniform minute intervals across hour boundaries.
3. Trap #2: Day-of-Month and Day-of-Week Use OR Semantics
Every field in a cron expression is combined with logical AND, with one POSIX exception: when both Day-of-Month (field 3) and Day-of-Week (field 5) are restricted (neither is *), standard Vixie cron matches the date if either field matches (logical OR).
# Intended: Run at midnight on Friday the 13th only
# Actual POSIX behavior: Runs on the 13th of EVERY month, AND on EVERY Friday!
0 0 13 * 5
To run a task strictly on Friday the 13th (or the first Monday of a month), trigger the cron on one condition and guard the second condition inside the command using date:
0 0 13 * * [ "$(date +\%u)" = "5" ] && /usr/local/bin/run-report.sh 4. Trap #3: Daylight Saving Time (DST) Skips and Double-Fires
If the host daemon runs in a local timezone that observes Daylight Saving Time (such as America/New_York or Europe/Berlin) rather than UTC:
- Spring Forward: Clocks jump from
01:59:59directly to03:00:00. A job scheduled for30 2 * * *(02:30 AM) does not run that day unless the cron implementation includes special DST catch-up logic. - Fall Back: The
01:00to01:59hour occurs twice. Naive schedulers can fire a15 1 * * *job twice on that Sunday.
Run production cron daemons, Kubernetes CronJob controllers, and database maintenance schedules in UTC (see Timezone Converter and Unix Timestamp Converter to map local windows to UTC).
5. Preventing Overlapping Executions
Cron launches a new process on schedule without checking whether the previous invocation finished. If a */5 * * * * job hits a slow database lock and takes 8 minutes, a second instance starts at minute 5, compounding load.
- Linux host crontab: Wrap commands with
flock -n /var/lock/myjob.lock /path/to/job.shso overlapping ticks exit immediately with a non-zero code. - Kubernetes CronJob: Set
concurrencyPolicy: Forbidin the manifest spec (default isAllow).