What happens at DST transitions
Cron schedules jobs by wall-clock time in the system's timezone. When DST kicks in, the wall clock jumps — usually forward 1 hour in spring and back 1 hour in fall. This creates two pathological windows:
| Transition | Effect | Example time that disappears or repeats |
|---|---|---|
| Spring forward (Mar in US) | 1 AM-3 AM hour disappears | 2:30 AM never occurs that day |
| Fall back (Nov in US) | 1 AM-2 AM hour repeats | 1:30 AM occurs twice |
If your cron job is scheduled within these windows, you have a problem.
Spring forward: skipped runs
Imagine your job: 30 2 * * * — every day at 2:30 AM. On the morning the clocks spring forward, the time goes:
1:58 AM → 1:59 AM → 3:00 AM
2:30 AM never happens. Most cron implementations silently skip the job on that day. For a daily backup, you've lost one run. For a billing reconciliation, you've potentially lost money.
How to detect it
Look at the log on Monday after a spring-forward Sunday. If your "every night at 2:30 AM" job has entries every day except Sunday's, this is likely the cause.
Fall back: duplicated runs
On the fall-back morning, the time goes:
1:58 AM (DST) → 1:59 AM (DST) → 1:00 AM (standard) → 1:01 AM (standard) → ... → 2:00 AM
The 1 AM-2 AM hour repeats. A job scheduled at 30 1 * * * fires twice — once at 1:30 AM DST and once at 1:30 AM standard.
Why this is worse than skipping
A skipped run is one missed execution. A duplicated run can cause:
- Duplicate emails sent
- Duplicate billing entries
- Race conditions if the previous run hadn't finished
- Corrupted data if the job isn't idempotent
Three strategies that work
Strategy 1: Run in UTC
UTC doesn't observe DST. If your system's timezone is UTC, the issue can never occur. Most cloud servers default to UTC for exactly this reason.
Trade-off: your 0 9 * * 1-5 will fire at 9 AM UTC, which shifts in local terms when DST changes. If you need "9 AM Pacific time always," you can't have that from a UTC server without adjusting the cron twice a year. Choose your poison: schedule drift or duplicated/missed runs.
Strategy 2: Avoid the DST window entirely
Schedule jobs outside the dangerous hours. For US-style DST (1-3 AM), schedule at 4 AM or later. Most batch jobs don't care whether they run at 2 AM or 4 AM:
# Risky: 30 2 * * * /path/to/nightly.sh # Safe: 30 4 * * * /path/to/nightly.sh
Strategy 3: Make the job idempotent + use anacron-style tracking
If you can't avoid the DST window, design your job to be safe under both skipping and duplication:
- Idempotent: running the job twice has the same effect as running it once. Use database transactions, "INSERT ... ON CONFLICT IGNORE", file locks, or a sentinel marker.
- Self-tracking: check the last successful run timestamp and skip if it's too recent (within 30 minutes, say).
#!/bin/bash LOCK=/var/run/nightly.lock if [ -f "$LOCK" ] && [ $(( $(date +%s) - $(stat -c %Y "$LOCK") )) -lt 1800 ]; then exit 0 # Ran within the last 30 minutes; skip fi touch "$LOCK" # ... actual work here ...
How each platform handles it
Vixie cron (most Linux)
For jobs scheduled within the lost hour on spring forward, Vixie cron will run them once immediately after the jump, as compensation. For the repeated hour on fall back, it does not run jobs twice — it remembers it already fired.
But: this only works if the cron daemon is running across the transition. If you restart the host during the transition, all bets are off.
systemd timers
Better behavior. systemd-timer supports OnCalendar=*-*-* 02:30:00 with a UTC qualifier to dodge DST entirely. It also has a Persistent=true option that ensures missed runs (due to DST or downtime) are caught up on next boot.
Kubernetes CronJob
Up through Kubernetes 1.27, CronJob ran in UTC always — no DST issue. From 1.27 onward, spec.timeZone allows non-UTC schedules, with the same DST hazards as system cron.
AWS EventBridge
Always UTC. EventBridge cron has no DST concept and never skips or duplicates due to it. The newer AWS EventBridge Scheduler service does support timezones if you need them.
Quartz (Java)
Configurable. The trigger's TimeZone property controls DST behavior. By default it follows the JVM's default timezone, which inherits from the OS.
Recommendation
For new jobs: run in UTC and adjust the cron expression to UTC equivalents. This is the only foolproof approach.
For existing jobs in a DST-observing timezone: move them out of the 1-3 AM window. Schedule for 4 AM or 5 AM instead. The few hours of "drift" between standard time and DST is rarely meaningful for batch work.
If you genuinely need the schedule to track local time precisely (e.g., business-hour user-facing jobs), use a real scheduler with built-in timezone support — systemd timers, AWS EventBridge Scheduler, or GCP Cloud Scheduler.