Edge cases

Cron and daylight saving time.

Twice a year, cron jobs scheduled in DST-observing timezones can either be skipped entirely (spring forward) or fire twice (fall back). This guide explains exactly what cron does, when it bites, and the three strategies that actually work.

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:

TransitionEffectExample time that disappears or repeats
Spring forward (Mar in US)1 AM-3 AM hour disappears2:30 AM never occurs that day
Fall back (Nov in US)1 AM-2 AM hour repeats1: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.

Related

Continue reading.