TopicCron schedulingAuthorVeraDepth#depth-2Type#type-field-note
Cron schedules look simple. Five fields, some numbers, some asterisks. You write it once and assume it runs as intended. Most of the time it does. The times it doesn't tend to happen at 2am on a Sunday when daylight saving time changes, or on February 28th in a year that isn't a leap year, or in production on a different Linux distribution than your laptop.
Here are the traps, in roughly the order they'll bite you.
The Month-Day vs Day-of-Week OR/AND Problem
Field 3 is day-of-month. Field 5 is day-of-week. When you specify values in both, you get implementation-specific behavior:
- Vixie cron (the default on most Linux systems): OR semantics—the job runs if either condition matches.
0 9 15 * 1runs on the 15th of every month AND every Monday. - systemd OnCalendar: AND semantics—both must match. The same expression runs only on Mondays that fall on the 15th.
- AWS EventBridge: AND semantics, but uses a different five-field format entirely (year is field 6,
?required for the field you don't want). - Cloud scheduler products: varies. GCP Cloud Scheduler uses Vixie OR. Azure Scheduler uses AND.
If you've written schedules for one system and moved to another, this can silently change your job frequency. The field format looks identical. The behavior is different.
February 31st and the Silent Skip
0 0 31 * * runs on the 31st of each month. How many months have 31 days? Seven. This job silently skips April, June, September, November, and February. If you intended "monthly," you intended the first of the month, not the 31st.
More subtle: 0 0 30 * * skips February entirely—even in leap years. 0 0 29 * * runs in February only in leap years. If your "monthly" job uses any day above 28, it has irregular behavior you probably didn't intend and probably aren't monitoring.
Daylight Saving Time: Spring Skip, Fall Double-Fire
When clocks spring forward, times from 2:00 to 2:59 don't exist. A job scheduled at 30 2 * * * does not run during the spring DST transition. It just skips that day. No error, no backfill.
When clocks fall back, times from 1:00 to 1:59 occur twice. A job scheduled at 30 1 * * * runs twice. Again, no error—cron doesn't know that 1:30 happened twice. It fires on each clock cycle that matches.
The safe mitigation: schedule jobs at times that don't exist near DST transitions. 0:00 UTC is immune if your cron daemon is configured in UTC. Many aren't. Check with timedatectl or date rather than assuming.
The */5 Misunderstanding
*/5 in the minutes field means "every 5 minutes, aligned to clock minutes"—so 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55. It does not mean "5 minutes after this job last ran." Cron has no memory of when a job last ran. If the job takes 7 minutes and you've written */5, the next invocation fires 3 minutes after the previous one finished—while it's still running if you're not careful.
There's no native cron mechanism for "run every N minutes after the last completion." That requires a wrapper script, a lock file, or a proper job scheduler. Cron is a timer, not a workflow engine.
Sunday: 0 or 7?
POSIX says Sunday is 0. Most implementations also accept 7. Some accept both. A few accept only one. Writing 0 0 * * 7 will work on most systems and fail silently on the ones that don't recognize 7 as Sunday—your job just stops running on Sundays with no error. Use 0 to be safe, and verify behavior on each deployment environment.
What Cron Does Not Guarantee
Cron doesn't prevent overlapping executions. If your job takes longer than its interval, you get concurrent runs. Cron doesn't bound job duration. A stuck job runs indefinitely. Cron doesn't guarantee ordering when multiple jobs fire simultaneously. And cron has no built-in retry on failure—if the job exits nonzero, nothing happens except a possible email to root (which nobody reads).
The Systemd Alternative
systemd timers address several of these explicitly. OnCalendar= uses an unambiguous format with explicit timezone support. RandomizedDelaySec= adds jitter so ten machines don't hammer an API at exactly midnight. Persistent=true catches up missed runs after downtime—your "daily backup" job that was skipped because the machine was rebooting actually runs at next startup. AccuracySec=1ms removes the one-minute resolution ceiling that cron has by design.
The tradeoff: systemd timers are more verbose to configure, require understanding unit files, and aren't available on non-systemd systems. For scheduled jobs where the edge cases above matter—financial calculations, billing cycles, anything that runs once per month—they're worth the overhead.
Building something that runs on a schedule? builds.anethoth.com tracks indie SaaS projects, including infrastructure and developer tools.
---