Twice a year, on the two daylight saving transitions, a cron job that has run faithfully every day for months can quietly skip a day or fire twice in the same hour. The job did not change, the crontab did not change, and nothing in the logs looks wrong until you go looking for the missing run. The cause is always the same: the scheduler is reading a local clock that springs forward and falls back, and an hour of that clock either does not exist or exists twice. Here is the exact mechanism, what the common schedulers do about it, and the one change that makes the problem go away for good.
The two failure modes: a lost hour and a repeated hour
Daylight saving creates two discontinuities in local time, and a daily job breaks differently on each.
Spring forward, the lost hour. When clocks advance, a block of local time simply never happens. In most of the United States on 8 March 2026, the clock reads 01:59:59 and the next second is 03:00:00. There is no 02:30 that day. A job scheduled at 02:30 local time has no instant to run at, so the scheduler has to decide whether to run it early, late, or not at all.
Fall back, the repeated hour. When clocks retreat, a block of local time happens twice. On 1 November 2026 in the same zone, the clock reaches 01:59:59 in daylight time, falls back to 01:00:00 in standard time, and replays the whole 01:00 to 01:59 hour. A job scheduled at 01:30 local time now has two valid instants to run at, and a naive scheduler runs it at both.
Everything else in this article is a consequence of those two facts. The danger window is narrow, usually one hour around 02:00 local on the transition night, but a daily job scheduled inside it is exposed every single year.
A worked example with real 2026 dates
Take a service running on a host set to America/New_York with two ordinary daily jobs in its crontab:
30 2 * * *: a database backup at 02:30 every morning.30 1 * * *: a billing run that finalises the previous day's invoices at 01:30 every morning.
On 8 March 2026 (spring forward), the local clock jumps from 02:00 to 03:00. The 02:30 backup falls inside the lost hour. On standard Linux cron it runs once, a few seconds after the clock reaches 03:00. On a scheduler that merely waits for the wall clock to read 02:30, the backup never runs that day, and you have a one-day hole in your backup history that nobody notices until a restore fails weeks later. The 01:30 billing run is before the transition, so it is fine on this day.
On 1 November 2026 (fall back), the local clock goes from 02:00 back to 01:00, replaying the 01:00 hour. Now the 01:30 billing run is the one in trouble. Standard Linux cron runs it once. A scheduler that fires on every wall-clock match runs it twice, once at 01:30 daylight time and once at 01:30 standard time, sixty minutes of real time apart. If that run charges cards or emits invoices without an idempotency key, every customer is billed twice for that day. The 02:30 backup is after the repeated hour, so it is fine this time.
Two jobs, two transitions, two completely different failures, and which job breaks depends only on whether its minute falls inside the hour that the clock skips or repeats. That is why the bug is so easy to miss: it never affects the same job on both transitions, and it never affects any job on the other 363 days of the year.
Why your scheduler might not save you
The classic Unix cron, descended from Paul Vixie's implementation and shipped as the default on most Linux distributions, actually tries hard to do the right thing. Its documented rule is that for jobs with a fixed hour and minute, a job in the skipped spring interval is run once soon after the change, and a job in the repeated autumn interval is run only once. If every scheduler behaved that way, this article would be much shorter.
The trouble is threefold. First, that special handling applies only to fixed-time jobs; a wildcard schedule such as */15 * * * * (every fifteen minutes) just follows the wall clock, so it loses four firings in spring and gains four in autumn, which is usually harmless but occasionally is not. Second, plenty of schedulers are not Vixie cron. Application-level libraries like Quartz, node-cron, APScheduler, and Celery beat each have their own interpretation of a crontab expression against a local time zone, and many of them do not implement the lost-hour and repeated-hour rules at all. Third, even on a correct cron, a job that itself reasons about local time, for example one that computes “yesterday” or sleeps until a wall-clock deadline, can still go wrong inside the transition even if cron launched it perfectly.
The practical takeaway is that you cannot assume the right thing happens. You have to know which scheduler you are on and what it does, or sidestep the question entirely.
How the major schedulers actually behave
Behaviour varies enough that it is worth knowing the specifics for the systems you are likely to be running:
- Vixie or ISC cron (most Linux hosts). Reads the system time zone. Fixed-time jobs in the skipped hour run once after the jump; fixed-time jobs in the repeated hour run once. Wildcard jobs follow the wall clock and so skip or repeat naturally.
- systemd timers.
OnCalendarexpressions are evaluated in the configured time zone and the manual defines the transition behaviour explicitly: a timer in the skipped hour elapses at the moment the clock jumps, and the repeated hour does not cause a second elapse. Generally the most predictable local-time option. - AWS EventBridge Scheduler. Runs in UTC unless you set a time zone. With a time zone configured, a schedule that lands in the skipped spring hour is not invoked, and a repeated-hour schedule is invoked once. Leaving it in UTC avoids both cases.
- Kubernetes CronJob. Historically evaluated in UTC against the controller's clock. Since Kubernetes 1.27 you can set
spec.timeZoneto an IANA name, at which point local-time transition rules apply. Without it, you are on UTC and safe. - Application schedulers (Quartz, Celery beat, node-cron, APScheduler).Each differs, and DST correctness is frequently a known sharp edge. Read the specific library's documentation, and prefer configuring them to UTC.
If you want to see how a given expression maps onto wall-clock times in a specific zone before you trust it in production, the cron time zone translator shows the local firing times for a crontab line in any zone, and the EventBridge cron converter translates a standard expression into the AWS dialect, which differs in subtle ways.
The fix: run everything in UTC
The durable solution is almost embarrassingly simple. UTC has no daylight saving and never will, so it has no lost hour and no repeated hour. A schedule expressed in UTC fires exactly once per day, every day of the year, on every scheduler, with no special cases to reason about. Setting your scheduler to UTC is the single highest-leverage change you can make to your batch infrastructure.
In practice that means three things. Set the host or container time zone to UTC, or set the scheduler's own time zone setting to UTC where it has one. Express every schedule in UTC, converting any desired local time once at design time. And make any job that needs to know “what day is it” compute that from a UTC instant plus an explicit zone, rather than from the ambient local clock. The 02:30 backup in New York standard time (UTC minus five) becomes 30 7 * * * in UTC, and once it is a fixed UTC time there is no lost or repeated hour for it to fall into.
The one real cost is drift. A job pinned to 07:30 UTC will read as 02:30 local in winter and 03:30 local in summer, because the local offset moves and UTC does not. For an overnight backup or a data export, nobody cares which side of the hour it lands on. The drift only matters when a human-facing deadline is involved, which is the next section.
When you genuinely need local time (and how to do it safely)
Some jobs really are tied to a wall clock. A report that must be in someone's inbox by 9am their time, a market-open task, a store that opens at a fixed local hour: these need to track local time through the transitions, not drift with UTC. For those, three rules keep you out of trouble.
First, keep the scheduled time out of the danger window. Nothing forces you to run at 02:30. Move a daily local-time job to a time that no transition touches, such as 04:30 or 05:30 local, and the lost and repeated hours become irrelevant because your job is never inside them. This alone defuses most real-world cases.
Second, make the job idempotent. A run that can safely execute twice with no extra effect, because it checks an idempotency key or a “already processed” flag before acting, cannot double-bill on the fall-back hour no matter what the scheduler does. This is good practice for retries and crash recovery anyway, so it pays for itself well beyond the DST edge.
Third, store the schedule against an IANA zone name, not a frozen offset. A schedule that says “08:00 in America/New_York” lets the system recompute the correct UTC instant on each side of a transition. A schedule that hard-codes “13:00 UTC because that is 08:00 Eastern today” is correct only until the next transition, then silently wrong by an hour. The same offset-versus-zone distinction governs recurring calendar invites, which is covered in detail in the daylight saving 2026 survival guide, and the underlying expression syntax is laid out in the cron expression examples guide.
A pre-transition checklist
Run through this list well before the next transition, not on the morning after a missed job:
- Set every scheduler and host you control to UTC unless a job has a hard local-time requirement.
- List every job whose minute falls between 00:30 and 03:30 local time, because those are the only ones a transition can touch.
- For each one, decide: move it out of the window, make it idempotent, or confirm your scheduler handles the lost and repeated hours the way you need.
- Verify the actual scheduler in use, not the one you assume. A container may not be Vixie cron, and an app library almost certainly is not.
- Add monitoring that alerts on a missing run, not just on a failing run, so a silently skipped job is visible the same day.
- For on-call shifts and rotations that hand off near 02:00, double-check the handover does not land in the skipped or repeated hour. The on-call rotation generator makes the boundaries explicit.
Frequently asked questions
Does cron understand daylight saving time?
Standard Linux cron (the Vixie or ISC lineage) reads the system local time zone and applies special handling for fixed-time jobs: a job in the skipped spring hour runs once just after the jump, and a job in the repeated autumn hour runs only once. But that special handling only covers jobs with a fixed hour and minute, it does not cover wildcard schedules, and many application-level schedulers do not implement it at all. The only behaviour you can rely on across every system is the one you get by running in UTC, which has no transitions.
Will a daily backup at 2:30am run on the spring-forward day?
It depends on the scheduler. On 8 March 2026 in US Eastern the clock jumps from 02:00 straight to 03:00, so 02:30 never occurs. Vixie cron runs the job once just after 03:00. A naive local-time scheduler that simply waits for the wall clock to read 02:30 will skip the job entirely that day, so the backup silently does not run. Schedule it in UTC, or at a time outside the 01:00 to 03:00 window, to avoid the question.
Can a cron job run twice because of daylight saving?
Yes, on the autumn fall-back day. On 1 November 2026 in US Eastern the clock goes from 02:00 back to 01:00, so every minute from 01:00 to 01:59 happens twice. A job fixed at 01:30 local time can fire twice on a scheduler that tracks the wall clock without DST-aware deduplication. If that job sends an invoice or charges a card, it double-bills. Running in UTC removes the repeated hour completely.
What is the simplest fix for daylight saving and cron?
Set the scheduler to UTC and convert your desired local run time to a fixed UTC time. UTC never springs forward or falls back, so a UTC schedule fires exactly once per day, every day, with no lost or repeated hours. The only cost is that a job pinned to a human-friendly local hour, like 9am for a business, will drift by an hour relative to the wall clock across the year, which is acceptable for most batch work.
How do AWS EventBridge and Kubernetes CronJobs handle the transitions?
AWS EventBridge Scheduler runs in UTC by default, and when you set a time zone it does not invoke a schedule that lands in the skipped spring hour and invokes a repeated-hour schedule once. Kubernetes CronJob historically evaluated schedules in UTC; since version 1.27 you can set spec.timeZone to an IANA name such as America/New_York, at which point the same lost-hour and repeated-hour rules apply to your local schedule. Check the exact version and configuration you are running rather than assuming.