Dev April 17, 2026 11 min read

Timezone Bugs in Production: DST, Leap Seconds, and tzdata

Real timezone bugs that shipped — the DST repeating hour, the 2012 leap second crash, tzdata release lag, and why 'just use UTC' doesn't fully save you.

Daylight Saving Transitions 1:00 1:30 2:00 → 1:00 1:30 (again) 2:00 This hour happens TWICE on fall-back Fall-back: naive timestamp parser produces wrong UTC twice 1:00 1:59 2:00 → 3:00 3:30 4:00 This hour is SKIPPED on spring-forward Spring-forward: scheduled 2:30 AM job runs at 3:30 AM or not at all

At 00:00 UTC on July 1, 2012, the International Earth Rotation Service added a leap second. The clock ticked from 23:59:59 to 23:59:60 instead of rolling to 00:00:00. Across the internet, things broke. Reddit went down. LinkedIn lost service. Yelp, FourSquare, Mozilla's build servers — all affected. The root cause was a Linux kernel bug that caused high CPU usage when the extra second was inserted, which cascaded into Java and MySQL crashes under load. Wired covered it the next morning. Two sysadmin communities traded war stories for weeks.

Time is one of those things that sounds simple and isn't. Every production system eventually ships a timezone bug. This post walks through the categories of these bugs — what breaks, why, and the shapes of the fixes — using real incidents that actually hit real systems.

The Repeating Hour: Fall-Back Ambiguity

When Daylight Saving ends, local clocks roll back one hour. In most US zones, 2:00 AM local time becomes 1:00 AM. The hour between 1:00 and 2:00 happens twice. A naive timestamp like 2026-11-01 01:30:00 America/Los_Angeles is genuinely ambiguous — it could refer to either occurrence.

Python's datetime module before Python 3.9 had no native way to disambiguate. pytz requires explicit is_dst handling, which most code skips. The result: some fraction of timestamps logged during that one hour each year have the wrong UTC equivalent, and nobody notices until the quarterly report shows a spike or dip in activity around 1:30 AM one Sunday in November.

# Python 3.9+ with zoneinfo
from datetime import datetime
from zoneinfo import ZoneInfo

# Ambiguous — which 1:30 AM?
dt = datetime(2026, 11, 1, 1, 30, tzinfo=ZoneInfo("America/Los_Angeles"))

# Explicitly before the fall-back (still daylight time)
dt_dst = dt.replace(fold=0)
# Explicitly after the fall-back (standard time)
dt_std = dt.replace(fold=1)

The fold attribute (PEP 495) explicitly disambiguates. fold=0 picks the earlier occurrence (still in DST), fold=1 picks the later one (back to standard time). Most code that should use this doesn't, because the writer didn't know the hour existed twice.

The Skipped Hour: Spring-Forward

The inverse problem. When DST begins, local clocks skip an hour — 2:00 AM becomes 3:00 AM directly. The hour between 2:00 and 3:00 doesn't exist. A scheduled job that's supposed to run at 2:30 AM on the spring-forward Sunday either runs at 3:30 AM, doesn't run at all, or (worst case) runs twice depending on the scheduler.

Cron handles this unpredictably across implementations. Systemd timers with OnCalendar are more explicit about the behavior. Kubernetes CronJobs just apply UTC internally, which sidesteps the problem — but then business-hour schedules ("run at 9 AM local time") need explicit conversion logic.

The 2012 Leap Second Incident

June 30, 2012 · 23:59:60 UTC

Linux kernel's hrtimer implementation hit an infinite loop after the inserted leap second. Java's System.currentTimeMillis() and applications depending on it (MongoDB, Cassandra, MySQL under certain load patterns) cascaded into high CPU usage. Reddit, LinkedIn, Yelp, Mozilla, FourSquare, and Gawker were among the sites affected. Recovery required rebooting affected servers or running date -s "$(date)" to reset the kernel's idea of time.

Leap seconds are inserted or removed by the International Earth Rotation Service to keep UTC aligned with astronomical time as Earth's rotation fluctuates. Since 1972, 27 leap seconds have been added. None removed. The next one might be announced six months in advance or might not happen for years — there's no schedule. Most recently, the 2022 leap second (December 31) passed without incident, largely because the ecosystem finally fixed most of the bugs from 2012.

In 2022, the IERS voted to abolish leap seconds by 2035. Until then, they remain a real consideration for any system that handles sub-second timestamps. The Google and Amazon approach — "smear" the leap second across a 24-hour window instead of inserting it atomically — works for cloud services but requires clients that tolerate a slightly-wrong second over that window.

tzdata Release Lag

The IANA tzdata database is the authoritative source of timezone rules. It's updated multiple times a year as countries announce DST changes, adjust offsets, or create new zones. Recent 2024 releases covered changes in Paraguay, Lebanon, and the Kingdom of Saudi Arabia. 2023 covered Mexico abolishing DST in most states (except states bordering the US).

Your language runtime or operating system ships with a specific tzdata version. Between releases, the data is frozen. A Linux server deployed in January 2024 has tzdata from that moment, and any country that changes DST rules before your server's OS gets updated will produce wrong local times until the update lands.

For languages like Python that bundle tzdata with the interpreter, the situation is worse — you get whichever version shipped with your Python install. The tzdata PyPI package (pip install tzdata) is a workaround that ships the current data. For Java, update to a recent JVM and use the tzupdater tool. For Node, the bundled ICU data determines timezone support; recent Node versions have reasonable coverage, but older ones can lag significantly.

Microsoft vs IANA Zone Naming

Windows uses its own timezone naming convention — names like Pacific Standard Time, Central European Standard Time, Korea Standard Time. These aren't IANA zone names. IANA uses America/Los_Angeles, Europe/Berlin, Asia/Seoul. Cross-platform code that serializes timezone info between a .NET backend and a Python service needs a conversion layer.

Microsoft publishes a mapping table, and the Unicode CLDR project maintains a more comprehensive windowsZones.xml that handles edge cases like territories with different actual zones mapping to the same Windows name. If your data crosses Windows and Unix hosts, pipe it through this translation explicitly — don't rely on either platform to guess correctly.

Python: pytz vs zoneinfo

For a long time, Python's timezone story was pytz. It works but has an API that's easy to misuse. The canonical pytz footgun:

# WRONG — this looks right but doesn't work
import pytz
from datetime import datetime

tz = pytz.timezone("America/Los_Angeles")
dt = datetime(2026, 6, 15, 14, 0, tzinfo=tz)  # Bug: uses LMT offset

# CORRECT with pytz
dt = tz.localize(datetime(2026, 6, 15, 14, 0))

# BEST: Python 3.9+ zoneinfo (PEP 615)
from zoneinfo import ZoneInfo
dt = datetime(2026, 6, 15, 14, 0, tzinfo=ZoneInfo("America/Los_Angeles"))

The first form looks reasonable but produces a datetime with the Local Mean Time offset from 1883 (about -7:52 for Los Angeles), not the correct -7:00 or -8:00 DST offset. pytz requires localize() for naive datetimes. This is one of the most common Python timezone bugs I've seen in production.

PEP 615's zoneinfo module (Python 3.9+) fixed the API. Pass the zone directly as tzinfo and it works correctly. Any new code should use zoneinfo; any legacy code on pytz is worth auditing for the localize() mistake.

JavaScript: Intl.DateTimeFormat Quirks

JavaScript's built-in Date object has no timezone support beyond the runtime's local zone and UTC. For anything else, Intl.DateTimeFormat with a named timezone is the modern approach:

// Format an existing Date in a specific zone
new Intl.DateTimeFormat("en-US", {
  timeZone: "Asia/Seoul",
  dateStyle: "full",
  timeStyle: "long"
}).format(new Date());

But parsing an ISO string into a specific timezone is still awkward. Libraries like date-fns-tz, luxon, or the newer Temporal API (currently Stage 3, available via polyfill) do this cleanly. Temporal is the right long-term answer — it makes timezone-aware datetimes first-class and avoids most of the pitfalls of the legacy Date. In the meantime, Luxon is the most battle-tested library for the task.

Java: Pre-8 Versus java.time

java.util.Date and java.util.Calendar are genuinely broken. They're mutable, have confusing month indexing (0-based!), and handle timezones poorly. If you're still using them, stop.

Java 8 (2014) introduced java.time, which is the JSR-310 API — a proper immutable date-time library. ZonedDateTime handles timezones correctly; Instant represents a moment in UTC; LocalDateTime is explicitly a timezone-less wall-clock reading. The distinction matters. Most legacy Java code that has timezone bugs is confusing these three types.

The "Just Use UTC" Myth

The advice is half-right. For storage, UTC is the correct answer. Every database timestamp, every log entry, every API response should be UTC. Converting to local time for display is cheap; recovering the correct UTC from a local time logged without zone info is often impossible.

But UTC doesn't help when the business rule is local. "Send the weekly report at 9 AM local time for each user" cannot be implemented as a UTC cron job. It requires zone-aware scheduling. "Charge this subscription on the 1st of the month" has to decide which timezone's 1st — the customer's? Your billing system's? — because the answer changes a day of revenue.

The full rule: store in UTC, compute in UTC, but present and schedule in the user's zone. And have an explicit answer for every business rule that says "local time."

A Checklist for Services That Handle Dates

  • Confirm your runtime's tzdata version. Pin or monitor it. Upgrade at least annually.
  • Never serialize a datetime without a timezone attached. "Naive datetime" should be banned from your API boundaries.
  • Use modern APIs: Python zoneinfo, Java java.time, JS Luxon or Temporal, Ruby ActiveSupport::TimeZone.
  • For scheduled jobs that care about local time, pick a cron implementation that's explicit about DST behavior. Kubernetes CronJobs run in UTC; systemd timers have a clearer local-time model.
  • Document which timezone your business rules refer to. "9 AM daily" without a zone is not specified.
  • Test your system against fall-back Sunday. If you can't produce correct results for events logged between 01:00 and 02:00 local time that day, you have an ambiguity bug.

For further reading, the canonical "Falsehoods Programmers Believe About Time" list (infiniteundo.com) is short, sharp, and humbling. Matt Johnson-Pint's writing on timezone change timing is the best deep reference I know of. And the IANA tzdata release notes are worth subscribing to if you operate services in countries that change rules periodically.

If your use case is scheduling specifically, our cron expressions guide covers the timezone surprises that hit scheduled jobs in more detail. And for counting days between dates in a way that handles DST correctly, the D-Day calculator is a good sanity-check tool.

Time will keep being hard. But most production timezone bugs fall into a handful of patterns that are fixable once you know to look for them.