edge-cases production dst reliability

Cron Edge Cases That Bite You in Production

by Sinthuyan  · April 13, 2026

Most cron expressions work exactly as expected 99% of the time. The 1% that doesn't surfaces during DST transitions, month boundaries, and other calendar anomalies — often in production, often silently. These are the edge cases I've seen cause real incidents.

DST Spring-Forward: The Missing Hour

When clocks spring forward (e.g. 2:00 AM becomes 3:00 AM), the 2:00 AM hour never exists. A cron job scheduled with 0 2 * * * is silently skipped on that day. For jobs that run daily, a single miss may be acceptable. For billing or settlement jobs, it's a serious problem.

The fix: schedule outside the DST danger zone. Run at 1:00 AM or 3:00 AM instead of 2:00 AM. Document the reasoning in a comment in the crontab.

DST Fall-Back: The Duplicate Hour

When clocks fall back (e.g. 2:00 AM becomes 1:00 AM again), the 1:00 AM hour occurs twice. A job at 0 1 * * * can run twice in one day — once before the clock change and once after. I've seen this cause duplicate charges in billing systems and double-sends in email pipelines.

If you must run during the fall-back window and duplicates are unacceptable, build idempotency into the job itself: check whether the job already ran today before doing any work.

Month-End Trap: Not Every Month Has 31 Days

The expression 0 0 31 * * runs only in months that have a 31st day: January, March, May, July, August, October, and December. April, June, September, November, and February are silently skipped. If your monthly report or invoice generation job uses day 31, it runs 7 times a year instead of 12.

Standard cron has no clean solution for "last day of the month." The common workaround is to schedule the job on day 28 and include a check inside the script that verifies it's the actual last day of the month before proceeding. Quartz scheduler solves this cleanly with 0 0 0 L * ? — the L character means "last day of the month" regardless of month length.

Leap Year: The 4-Year Silent Failure

The expression 0 0 29 2 * runs only on February 29 — which occurs only in leap years. If a billing cycle or compliance report is scheduled here, it silently skips for three consecutive years and then runs once. The failure won't surface in testing unless your test specifically simulates a non-leap year.

Step Value on Month Field

The expression 0 0 1 */3 * runs on the first of every third month, starting from January: months 1, 4, 7, 10 (January, April, July, October). It does not mean "every 3 months from the current month." Step values always calculate from the start of the field's range, not from the current time.

DOM + DOW Conjunction: OR, Not AND

In standard cron, if you specify both a day-of-month value and a day-of-week value, the job runs when either condition is true — not both. The expression 0 0 1 * 1 runs on the first of every month and also on every Monday. This OR behavior is counterintuitive and differs from Quartz, which forbids specifying both fields simultaneously (requiring ? in one of them).

High-Frequency Limits

Standard cron has 1-minute resolution. The finest expression you can write is * * * * * — once every minute. If you need sub-minute scheduling, cron is the wrong tool. Use systemd timer's AccuracySec with a OnCalendar unit, an application-level scheduler with second precision, or a queue worker that processes tasks as fast as they arrive.

Attempting to simulate sub-minute scheduling with cron by chaining sleep loops inside a script (* * * * * sleep 30 && /my/job) is fragile and leads to overlapping executions when the job takes longer than 30 seconds.

Related Guides