/
July 24, 20266 min read

5 Real Cron Scheduling Bugs That Actually Happened

SMTWTFS!one wrong assumption, one bad day

Most cron mistakes aren't exotic — they're the same handful of patterns causing trouble over and over, in ways that don't show up until the specific edge case they depend on actually occurs. These five are composites drawn from the kinds of scheduling bugs that show up constantly in incident postmortems and troubleshooting threads — the details vary, the underlying mistake almost never does.

The double-charge that came from */45

A billing job was scheduled with */45 * * * * on the reasonable-sounding assumption that it meant 'every 45 minutes.' It doesn't — cron step values only produce a true rolling interval when the step evenly divides the field's range, and 45 doesn't divide 60. The actual schedule was minute 0 and minute 45 of every hour: a 45-minute gap, then a 15-minute gap, repeating. A downstream batch job that assumed a consistent 45-minute window between runs ended up processing overlapping time ranges during the short gap, and a subset of transactions got billed twice.

The fix wasn't a scheduling change — it was recognizing that */45 never meant what it looked like it meant, and that any step value needs to be checked against whether it actually divides its field's range evenly before it's trusted to mean 'every N.'

The backup that silently ran twice a year

A nightly job scheduled for 1:30 AM server-local time ran fine for months, then started running twice on one specific night and not at all on another, exactly twice a year. The server's local timezone observed daylight saving time, and cron has no awareness of DST at all — it just watches the wall clock. When clocks fall back, the 1:30 AM wall-clock time occurs twice, and cron dutifully fires for both; when clocks spring forward, 1:30 AM never occurs that day, and the run is silently skipped entirely.

The fix that actually holds up is running servers on UTC and scheduling cron in UTC too, then converting to local time only for display purposes — DST transitions become a non-event because UTC doesn't observe them.

The report that ran on the wrong Saturdays

A job meant to run on the last weekday of each month combined a day-of-month range with a day-of-week value, expecting cron to treat it as an AND condition where both must match. Standard cron's day-of-month and day-of-week fields are OR'd together whenever neither is a wildcard, so the job actually ran on every day matching either condition, producing extra runs on weekdays that had nothing to do with month-end.

This is one of the most consistently misunderstood parts of cron syntax, and the only reliable fix is knowing the OR-rule exists in the first place — when you need a true AND between day-of-month and day-of-week, the check has to move into the job itself.

The sync job that fell 90 minutes behind after a reboot

A data-sync job ran every 5 minutes and was assumed to always be roughly current. After a routine kernel-update reboot took the server offline for about half an hour, nobody noticed the job hadn't caught up on the runs it missed — plain cron doesn't remember or replay anything it missed while the machine was off. The gap turned into a much larger reconciliation problem once it was finally noticed, because nothing had flagged that the sync had silently fallen behind.

anacron, for periodic jobs on machines that sleep or reboot, or a monitoring heartbeat that alerts when an expected run doesn't arrive, are the two practical fixes — either catch up automatically, or make the gap loud enough to notice immediately.

The Quartz migration that silently dropped every job

A team migrating standard cron expressions into a Quartz-based Java scheduler copied the 5-field expressions over as 6-field Quartz ones by just prepending a 0 for seconds, without noticing that Quartz requires exactly one of the day-of-month or day-of-week fields to be the literal ? character, and rejects expressions where both are concrete values. Every migrated job failed to parse, and because the deployment process didn't surface scheduler startup errors anywhere visible, the entire batch of previously-working jobs silently stopped running the moment the new deployment went live.

The fix is procedural as much as technical: treat a cron-format migration between platforms as a real conversion with its own validation step, not a mechanical field-count change, and add a startup check that fails loudly if any scheduled job didn't register successfully.

Key Takeaways

  • Step values (*/N) only produce a true rolling interval when N evenly divides the field's range — check this before trusting */45-style schedules.
  • Run servers and cron schedules in UTC; local-timezone scheduling silently breaks twice a year around DST transitions.
  • Cron ORs day-of-month and day-of-week when both are specified — it never ANDs them, a frequent source of 'ran on the wrong days' bugs.
  • Cron never catches up on runs missed during downtime — pair anything critical with anacron or a heartbeat alert.
  • Treat cross-platform schedule migrations as a real conversion needing validation, not a mechanical syntax tweak.

Frequently Asked Questions

Are these bugs specific to one platform?
No — the underlying mechanics (step-value math, DST, the day-of-month/day-of-week OR-rule, no catch-up after downtime, format differences between cron dialects) apply to standard Linux cron and most of its derivatives alike. The specific fix varies by platform, but the root causes are the same everywhere.
What's the single highest-leverage habit for avoiding these?
Validate every non-trivial expression before deploying it — check what it actually matches, not just what it looks like it should match, especially for step values and combined day-of-month/day-of-week schedules.
How do I check whether a schedule will misbehave around DST?
Run your servers and crontabs in UTC. It's not just a mitigation — it removes the entire class of DST-related scheduling bugs, since UTC has no daylight saving transitions at all.