How to Handle Timezone and Daylight Saving Bugs in Scheduled Data Automation Jobs

A network operations center with rows of monitoring screens

Twice a year, in most of the United States and Europe, a specific class of scheduled job either runs twice or doesn't run at all. It's not a code deploy that caused it, and it's not a server outage. It's the daylight saving transition, and it catches teams off guard with a predictable regularity that should make it boring by now. It usually doesn't, because the bug is invisible for 363 days a year and only shows its face on two specific dates most people don't have circled on a calendar.

Why "run at 2am" is a more dangerous instruction than it looks

A cron schedule like 0 2 * * * reads as "run at 2am every day," and for 363 days a year, that's exactly what happens. On the day clocks spring forward, 2:00am doesn't exist. The clock jumps from 1:59am directly to 3:00am, and depending on how your scheduler interprets that missing hour, your job either gets skipped entirely for that day or fires immediately when the clock catches up, at a time nobody planned for. On the day clocks fall back, 2:00am happens twice, and a naive scheduler can fire the job twice, once during each occurrence of that hour.

This is worse for jobs that read data based on "yesterday" or "the last 24 hours," since a double-run or a skipped run doesn't just misfire once, it can produce a window calculation that's off by an hour in either direction, which silently corrupts whatever aggregation depends on clean day boundaries.

The fix that actually works: schedule in UTC, convert for humans

The most reliable fix is to never schedule a job based on local wall-clock time at all. UTC has no daylight saving transitions, so a job scheduled at a fixed UTC time runs at a mathematically consistent interval, 24 hours apart, every single time, with no ambiguity twice a year. The complexity moves to a place you can control: converting the business requirement ("run at 2am local time for the sales team") into a UTC schedule once, explicitly, and documenting which local time it corresponds to.

illuminated world map wall clock display
Photo by Ebahir on Pexels

The catch is that a UTC-fixed schedule tied to a specific local time will itself drift relative to that local time across the DST boundary, since UTC doesn't observe DST but most local business hours implicitly assume it does. If "2am local" genuinely needs to track local wall-clock time year-round (some reporting requirements do), you need your scheduler to be timezone-aware, not just UTC-fixed, and to explicitly handle the two edge cases: the nonexistent hour at spring-forward, and the repeated hour at fall-back.

What timezone-aware scheduling libraries actually do differently

Modern scheduling libraries that claim timezone awareness (rather than just accepting a UTC offset) generally handle the ambiguous cases in one of two ways: they either pick a fixed, documented rule (always use the first occurrence of an ambiguous local time, always skip a nonexistent one) or they expose the ambiguity to your code and force you to decide. Neither is "correct" in some universal sense. What matters is that your team knows which behavior your specific tool implements, instead of assuming it matches whatever intuition seems most obvious, because the two reasonable choices produce genuinely different results on exactly two days a year.

"The DST bug is almost a rite of passage for anyone who's owned a scheduling system long enough. Everyone builds the naive version first, it works for eleven months, and then March happens." - Dennis Traina, founder of 137Foundry

Testing this before it happens in production

You don't have to wait for the actual transition date to find this bug. Most timezone libraries let you construct a specific datetime and ask what happens across a DST boundary without waiting for the calendar to catch up. Writing a test that explicitly constructs "the day before spring-forward" and "the day before fall-back" for your job's timezone, then asserting your scheduler produces exactly one run in each case, catches this months before it would otherwise surface as a support ticket about missing or duplicated data.

server room with blinking status indicator lights
Photo by Matthieu Beaumont on Unsplash

This is a case where the cost of testing is genuinely tiny relative to the cost of not testing. The test takes an afternoon to write once. The alternative is a recurring, twice-yearly incident that shows up as a confusing data gap or duplicate, gets investigated from scratch each time because nobody remembers the root cause from six months ago, and erodes trust in whatever report or dashboard depends on that job's output.

Python's zoneinfo makes the ambiguity explicit instead of hiding it

If your automation stack is Python, the standard library's zoneinfo module (and the fold attribute on datetime objects) is the modern way to represent an ambiguous local time explicitly rather than silently picking one interpretation for you. fold=0 means the first occurrence of an ambiguous time, fold=1 means the second, and code that ignores fold entirely is implicitly choosing fold=0 without saying so. The older pytz library handled this differently and inconsistently enough that a lot of subtle DST bugs in Python codebases trace back to a pytz-based datetime being passed through a code path that assumed zoneinfo semantics, or vice versa. If your codebase has a mix of both, that mismatch is worth auditing on its own, independent of the scheduler question.

Cron itself has no timezone concept at all

It's worth being explicit that traditional cron, the actual /etc/crontab mechanism, has no built-in timezone awareness whatsoever. It runs on whatever timezone the host operating system is configured with, which means the "same" cron expression can mean a different wall-clock time depending on which server it's deployed to, and a server's OS timezone setting is exactly the kind of configuration detail that's easy to get wrong once during provisioning and never revisit. Container-based deployments make this worse in a specific way: a container image built with one base timezone, deployed onto hosts with another, can produce a scheduling discrepancy that has nothing to do with DST at all and everything to do with the base image configuration. Checking date inside your actual production container, not just on your laptop, is a five-second sanity check worth doing before debugging anything more exotic.

Tools like crontab.guru are useful for decoding what a cron expression actually means, but remember that they can only tell you what the expression parses to, not what timezone it'll execute in on your specific host. That second part still has to be verified against the actual machine, not inferred from the expression syntax alone.

Cloud schedulers hide the same decision behind a friendlier interface

Managed schedulers (cloud cron jobs, workflow orchestrators, CI/CD pipeline schedules) tend to present a timezone dropdown in their UI, which makes the choice feel safer than raw crontab, but the underlying ambiguity at the DST boundary doesn't go away just because there's a nicer interface in front of it. Most of these platforms document, somewhere in their less-visited pages, exactly which of the two reasonable behaviors they implement for the skipped and repeated hours. It's worth finding that documentation once per platform you use, rather than assuming every scheduler you've ever touched behaves identically, because they genuinely don't all agree, and a team running jobs across two or three different platforms can easily end up with two or three different DST behaviors without anyone deciding that on purpose.

Downstream systems need to be idempotent regardless

Even with a perfectly timezone-aware scheduler, it's worth building the job itself to tolerate an accidental double-run, since schedulers, however careful, aren't the only thing that can cause a duplicate execution. A deploy that restarts a worker mid-run, a retry after a transient network failure, or a manual re-trigger during an incident can all produce the same "ran twice" scenario a DST bug produces. If the job's output is idempotent, keyed on a natural identifier so re-running it with the same input produces the same result rather than a duplicate, the DST edge case stops being a crisis and becomes a non-event even on the two days a year it would otherwise bite.

What to actually do this week

  • Audit your scheduled jobs for any that use a bare local-time cron expression without explicit timezone handling.
  • For anything tied to a genuine business requirement for local time, confirm your scheduler's library explicitly documents its DST-ambiguity behavior, don't assume.
  • Write the two edge-case tests (day before spring-forward, day before fall-back) for at least your highest-impact scheduled jobs.
  • Make sure the job's own logic tolerates a duplicate trigger gracefully, since scheduler correctness is only half the protection you actually need.

If your team is building out a broader data automation practice and this is one gap among several, 137Foundry's data integration work covers pipeline reliability patterns like this one as part of a wider audit, alongside things like idempotency and retry design. For the technical reference on how UTC and DST actually interact, IANA's time zone database documentation is the canonical source every major scheduling library ultimately depends on, and Wikipedia's overview of daylight saving time is a good primer on which regions observe it and when the transition dates actually fall. Python's official zoneinfo documentation covers the fold attribute and ambiguous-time handling in more depth if that's the stack you're debugging this in. You can read more engineering deep dives like this one on the 137Foundry blog, or see the full range of what we build on the services page.

Need help with your next project?

137Foundry builds custom software, AI integrations, and automation systems for businesses that need real solutions.

Book a Free Consultation View Services