A cron job scheduled for 2:30 AM doesn't run on the night clocks "spring forward" — because 2:30 AM doesn't exist that night — and the same job might run twice on the night clocks "fall back," because 2:30 AM happens twice
The previous articles on this site covered cron syntax, the cron daemon's internals, and modern scheduling alternatives. This article addresses a specific, recurring source of "why did this job not run last night" (or "why did it run twice") confusion: cron jobs scheduled in local time, on systems that observe Daylight Saving Time (DST) transitions.
The "spring forward" problem: a time that doesn't exist
When clocks "spring forward" (e.g., 2:00 AM becomes 3:00 AM, in one "jump" — the hour from 2:00-2:59 AM doesn't occur that night) — a cron job scheduled for, say, 30 2 * * * (2:30 AM daily) has no 2:30 AM to run during on this specific night — the clock goes directly from 1:59:59 to 3:00:00, skipping the 2:XX hour entirely.
What happens to the job depends on the cron implementation:
- Some implementations skip the job entirely for that night — the job simply doesn't run, and resumes its normal schedule the following night (when 2:30 AM does exist again)
- Other implementations run the job immediately when the clock "catches up" — e.g., running the job at 3:00 AM (or shortly after), treating the "missed" 2:30 AM trigger as something to "catch up on" as soon as possible after the jump
Neither behavior is "wrong" per se — but the specific behavior depends on the cron implementation/configuration — and a job expecting to run exactly once per day, at exactly 2:30 AM, might, on the "spring forward" night, either not run at all (if the implementation skips) or run at an unexpected time (if the implementation "catches up") — either of which could be surprising/problematic depending on what the job does and what assumptions downstream processes make about "this runs at 2:30 AM, exactly once per day."
The "fall back" problem: a time that occurs twice
When clocks "fall back" (e.g., 2:00 AM, upon reaching 3:00 AM, reverts to 2:00 AM — the hour from 2:00-2:59 AM occurs twice that night) — a cron job scheduled for 30 2 * * * (2:30 AM) has two opportunities to "match" 2:30 AM that night — the first occurrence of 2:30 AM (before the fall-back), and the second occurrence (after the fall-back, when the clock reaches 2:30 again).
Depending on the implementation: the job might run twice, once at each "2:30 AM" — or the implementation might have specific logic to avoid double-execution (e.g., tracking "have I already run for "this" 2:30 today?" in a way that accounts for the repeated hour).
*A job not designed to be idempotent (covered in the previous "production scheduled jobs" article — idempotency meaning "running this multiple times has the same effect as running it once") running twice, on "fall-back" night, could cause duplicate processing — duplicate emails sent, duplicate records created, or other consequences of "this ran when it shouldn't have."
The "obvious" fix: UTC, and why it's the recommended approach for most automated systems
Configuring cron (and the underlying system) to operate in UTC, rather than a local, DST-observing timezone — eliminates both problems entirely — UTC doesn't observe DST — every day has exactly 24 hours, every hour occurs exactly once, every day, with no "skipped" or "repeated" hours ever.
A cron job scheduled in UTC (e.g., 30 2 * * * where the system's cron daemon operates in UTC) runs at exactly 2:30 UTC, every day, unconditionally — no DST-related edge cases exist, because UTC has no DST.
This is part of why server systems (and cloud infrastructure generally) are commonly configured to run in UTC, even for organizations/teams based in DST-observing timezones — separating "what timezone the server's clock/scheduler operates in" from "what timezone the humans using the system are in" — the server (and its cron jobs) operate in UTC; humans viewing "when did this run" logs can have UTC timestamps converted to their local timezone for display — but the underlying scheduling itself remains DST-immune.
When local time scheduling is intentional: "run at the start of the business day, wherever that is"
Sometimes, "run this job at local time" is, genuinely, the intended behavior — e.g., "send a daily summary email at 9 AM, the recipient's local time" — here, the whole point is that the job's trigger time should track "9 AM, local,"* including through DST transitions (the email should still arrive at "9 AM local" after a DST change, not at "the same UTC time as before the change," which would, post-DST-change, correspond to a different local time).
For such "intentionally local-time" scheduling — the DST-transition-night edge cases (skipped/doubled hour) are, to some extent, unavoidable given the fundamental nature of DST — but understanding that they will occur, roughly twice a year, on predictable dates (the specific DST-transition dates, which vary by country/region) — allows for deliberate handling: idempotency (so a "double-run" on fall-back night doesn't cause duplicate effects), and, for jobs where a "skipped" run (on spring-forward night) would be problematic — additional monitoring/alerting specifically checking "did this job run as expected, particularly around known DST transition dates."
Different countries' DST transitions happen on different dates
A further complication for systems serving users across multiple timezones/countries: DST transitions don't happen on the same date everywhere — different countries/regions (and, historically, even different jurisdictions within the same country, in some cases) have transitioned on different dates, and some regions don't observe DST at all.
For a system with "send at 9 AM, recipient's local time" jobs, serving recipients across multiple regions' local times — each region's "9 AM local" job needs to independently account for that region's own DST schedule (which, as noted, might differ from, or not exist at all for, other regions) — this is generally handled by timezone-aware scheduling libraries (which incorporate up-to-date timezone databases — the widely-used "tz database"/"IANA timezone database," which tracks historical, current, and (to the extent known in advance) future DST rules for every recognized timezone) — rather than naive "cron-expression-in-local-time" approaches, which typically operate relative to a single, system-wide timezone setting, not per-recipient timezones.
How to use the Cron Explainer on sadiqbd.com
- For most automated/server-side jobs: verify what timezone the cron daemon/system operates in — UTC is generally recommended unless there's a specific, intentional reason for local-time scheduling
- For jobs scheduled in the "2 AM hour" specifically (a common time for maintenance jobs, backups, etc. — chosen, often, precisely because it's "low-traffic" overnight): be aware this hour is exactly the hour most commonly affected by DST transitions (in regions observing DST with the typical "spring forward/fall back" occurring around this hour) — if operating in a local, DST-observing timezone, jobs in this window are the most likely to encounter the skipped/doubled-hour issues described
- For jobs requiring "local time, wherever the recipient is": consider whether a timezone-aware scheduling approach (per-recipient timezone handling, via a library incorporating the IANA timezone database) is more appropriate than a single, system-wide cron schedule
Frequently Asked Questions
If my server is configured for UTC, but I want a report generated at "9 AM New York time," how do I handle DST?
You'd need to calculate, periodically (or dynamically), what UTC time corresponds to "9 AM New York time" — and this calculation's result changes across DST transitions (9 AM EST ≠ 9 AM EDT, in UTC terms — they're 1 hour apart in UTC, even though both represent "9 AM, New York"). A naive, static cron schedule in UTC (e.g., 0 14 * * *, which is 9 AM EST in UTC) would correspond to 8 AM EDT (during daylight time), not 9 AM — a static UTC cron expression doesn't track "9 AM New York" through DST changes — for this, you'd need either (a) two different cron schedules, switched manually (or via automation) around DST transition dates, or (b) a timezone-aware scheduling library that computes the correct UTC trigger time dynamically, accounting for the target timezone's current DST status — option (b) is generally more robust and less error-prone than manually maintaining (a).
Is the Cron Explainer free? Yes — completely free, no sign-up required.
Try the Cron Explainer free at sadiqbd.com — translate any cron expression to plain English, and check scheduling against UTC.