Skip to content
Published on

The Technical Hell of Time and Date — TimeZone, DST, Leap Seconds, Temporal API, NTP Complete Guide (2025)

Authors

Prologue — "October 27, 2024, 02:30" Might Not Exist

On the day summer time ends in Europe, when 03:00 arrives, the clock falls back to 02:00. That means 02:30 appears twice that day. If a coupon was issued in an event at 02:30 that day, which 02:30 does the coupon belong to?

Conversely, on the last Sunday of March, Europe jumps from 02:00 to 03:00. The time 02:30 does not exist at all. If a user saves "reservation at 02:30," when will it run?

These aren't just UX issues. Order sorting in financial trading, chronological log analysis, event ordering in distributed systems, payroll calculations — real systems break on this subtlety. Jon Skeet (author of Noda Time) famously said: "Time is a joke. Seriously."

This article explains the technical foundations of handling time and date from scratch. What UTC is and how it differs from GMT, why Unix time ignores leap seconds, why the IANA time zone database gets updated frequently, how to handle DST transitions safely, the infamous pitfalls of new Date(), why the JavaScript Temporal API is a once-in-a-decade revolution, how NTP/PTP synchronize clocks in distributed systems, and even Google Spanner's TrueTime.

If the Unicode article said "strings are a hard problem," time is harder. Strings are at least deterministic within a single computer, but time is entangled with Earth's rotation changes, political decisions by nations, and OS clock drift.


1. Three Concepts of Time — Instant, Local, Civil

When dealing with time in programs, the three distinctions you must make first:

1.1 Instant

The absolute point "right now." No matter where on Earth you observe, the value is the same. Unix timestamp (seconds elapsed since 1970-01-01T00:00:00Z) is the representative form.

Example: 1760000000 (the epoch in seconds corresponding to 2025-10-09T07:33:20Z)

1.2 Civil Date/Time

Values read from a calendar and clock, like "April 15, 2026, 13:30." Ambiguous without a time zone.

1.3 Zoned Date/Time (civil time attached to a time zone)

"2026-04-15 13:30 Asia/Seoul". Now convertible to an Instant.

1.4 Core Rules

  • DB/logs/sorting: store as Instant (UTC point)
  • UI display: convert to user's time zone as Zoned
  • Recurring schedules ("9 AM on the first of every month") should be stored as Civil + TimeZone

"Just store in UTC" is half right and half wrong. If a Seoul user sets "alert at 00:00 on December 25, 2026," storing it converted to UTC may cause the date to change under DST shifts or time zone changes. In such cases you must preserve the original civil+tz.


2. UTC, GMT, TAI, UT1 — The Confusion of Time Standards

2.1 GMT (Greenwich Mean Time)

Mean solar time at Greenwich, UK. Standardized in the 19th century for maritime navigation. Legally still used in some UK official documents, but in technical writing "GMT" is almost always a colloquial synonym for UTC.

2.2 UTC (Coordinated Universal Time)

The international standard. Based on atomic time, extremely precise, but inserts leap seconds to align with Earth's rotation. UTC = TAI - (accumulated leap seconds).

2.3 TAI (International Atomic Time)

Average of about 400 atomic clocks worldwide. No leap seconds. As of 2025 it is 37 seconds ahead of UTC. GPS time is based on TAI (not UTC), with a 1980 reference offset.

2.4 UT1

Time actually aligned with Earth's rotation. UTC must stay within 0.9 seconds of UT1, which is why leap seconds are inserted.

2.5 Practical Summary

  • What programs call "UTC": usually UTC = epoch + seconds (ignoring leap seconds)
  • Strict UTC: includes leap seconds
  • For everyday use the difference is negligible, but it matters in financial high-frequency trading, satellite communication synchronization, etc.

3. Unix Timestamp — The Price of Simplicity

3.1 Definition

Seconds (or ms/ns) elapsed since 1970-01-01T00:00:00Z.

  • POSIX standard: seconds, leap seconds ignored
  • That is, a day is treated as exactly 86,400 seconds

3.2 Where Do Leap Seconds Go?

When a leap second is inserted in real Earth time, UTC passes through 23:59:60 once. How does Unix time handle this?

Two approaches:

  1. Slew (recommended): slowly stretch time over several hours before and after the leap second to absorb it. Used by Google/AWS/Meta. No moment is "skipped."
  2. Smear: the NTP server slews so that the leap-second day runs roughly 86,401 seconds instead of 86,400. The OS clock progresses smoothly.
  3. Jump: jump back from 23:59:59 to 00:00:00. Many systems crash or generate duplicate events.

Historical incidents:

  • June 30, 2012 leap second: a Linux kernel bug caused many Java applications to consume 100% CPU. Reddit, LinkedIn, Foursquare went down
  • June 30, 2015 leap second: brief outages at Twitter/Instagram
  • January 1, 2017 leap second: Cloudflare DNS had partial outages due to a leap-second calculation error

Future: in 2022 the General Conference on Weights and Measures (CGPM) decided to abolish leap seconds by 2035. After that, adjustments will be made on a scale of decades using "leap minutes" or similar coarser units.

3.3 The Year 2038 Problem

If Unix time is stored as a 32-bit signed integer, it overflows after 2038-01-19T03:14:07Z. A time bomb for 32-bit embedded systems (IoT, legacy database formats).

Mitigation: most modern OSes/languages have moved to 64-bit, but legacy C/C++ binaries, some DB schemas, and file formats must be audited. Verify time_t size explicitly.

3.4 Year Zero Issue vs Negative Epoch

-62135596800 is 1 AD January 1 UTC. Earlier dates (BCE) are representable or not depending on the protocol. Java's Instant.MIN is -9999999-01-01, Instant.MAX is 9999999-12-31.


4. ISO 8601 — The String Representation Standard

4.1 Basic Formats

2026-04-15                       date
2026-04-15T13:30:00              local time
2026-04-15T13:30:00Z             UTC
2026-04-15T13:30:00+09:00        Seoul time
2026-04-15T13:30:00.123456789Z   nanosecond precision
2026-W16-3                       week-based (Wednesday of ISO week 16)
P1Y2M10DT2H30M                   duration (1y 2mo 10d 2h 30m)
2026-04-15/2026-04-20            interval

4.2 RFC 3339 — A Subset of ISO 8601

A subset restricted by IETF for internet protocols. Lowercase T allowed, time zone offset mandatory, etc.

4.3 Everyday Pitfalls

  • 2026-04-15T13:30:00 (no zone) — local? UTC? Parsers differ (JavaScript new Date() uses local; Python fromisoformat returns naive)
  • Z vs +00:00 — same meaning but string comparison differs
  • Sub-second precision varies by system (ms vs μs vs ns)
  • +09 vs +0900 vs +09:00 — parser support varies

Lesson: when exchanging as strings, always include the time zone offset explicitly. Use official ISO 8601 parsers (Java Instant.parse, Python 3.11+ fromisoformat, JS Temporal.Instant.from).


5. Time Zones — The Politics of Time

5.1 What Time Zones Do

They determine the offset from UTC. But the offset:

  • Varies by region
  • Changes historically (countries adopt/repeal DST, change offset)
  • Can change suddenly by political decision

5.2 IANA Time Zone Database (tz database, zoneinfo, Olson DB)

Maintained by Paul Eggert since 1986, the textbook of time-zone history. Keyed by names like Asia/Seoul, America/New_York, it contains every offset, DST rule, and historical change that region has ever used.

Example: Asia/Seoul changed from UTC+8:30 to UTC+9 on March 21, 1954. The offset shifted several times until 1961, and DST existed from 1987 to 1988 before being abolished. To convert "January 1, 1955, 00:00 Seoul" to UTC today, an API must know this history.

5.3 tzdata Updates

New versions appear 5–10 times per year. Recent changes:

  • 2024: Egypt reintroduced DST
  • 2023: most of Mexico abolished DST
  • 2022: Chile changed DST dates
  • 2022: Jordan and Syria moved to permanent DST

Operational importance:

  • Check the tzdata version in your container image (alpine requires installing the tzdata package separately)
  • JVMs can be updated with TZupdater
  • If a DB server's OS tzdata is stale, the app uses wrong offsets
  • Node.js bundles ICU, but some slim distributions only ship small-ICU — limited time zones (set NODE_ICU_DATA)

5.4 Ambiguity of Abbreviations

CST — which country?

  • Central Standard Time (US, UTC-6)
  • China Standard Time (UTC+8)
  • Cuba Standard Time (UTC-5)

IST — even worse:

  • India Standard Time (UTC+5:30)
  • Ireland Standard Time (UTC+1)
  • Israel Standard Time (UTC+2)

Rule: use abbreviations for display only. In programs, use IANA names (Asia/Seoul, America/Chicago).

5.5 POSIX TZ Strings

An older format but still alive: EST5EDT,M3.2.0,M11.1.0. Never use it — use IANA names.


6. DST — The Live Minefield

6.1 "The Time That Doesn't Exist"

March 31, 2024 in Europe: 02:00 jumps to 03:00. 02:30 does not exist.

// JavaScript, if you don't know what you're doing
new Date('2024-03-31T02:30:00+01:00').toISOString()
// "2024-03-31T01:30:00.000Z" — not the 02:30 you intended

Handling:

  • If a user's civil time doesn't exist, either error out or forward shift (to the next valid time)
  • Java ZonedDateTime.of(...) + ZoneRulesProvider can handle Gap
  • Temporal.ZonedDateTime.from({..., disambiguation: 'earlier'/'later'/'reject'/'compatible'})

6.2 "The Time That Appears Twice"

October 27, 2024 in Europe: 03:00 falls back to 02:00. 02:30 exists twice.

  • The same string "2024-10-27 02:30 Europe/Berlin" maps to two UTC instants
  • Financial logs should be sorted in UTC to be safe

6.3 Calendar Reservation Disaster

If "Monday 9:00 AM weekly meeting" is stored converted to UTC:

  • Seoul (always UTC+9) is UTC 00:00
  • New York (with DST) is UTC 14:00 or UTC 13:00 (different in summer/winter)

Storing the original civil+tz is the correct answer. Split DB columns into (timestamp, timezone, recurrence_rule).

6.4 The Trend Toward Abolishing DST

The EU decided in 2019 to "abolish DST from 2021," but it was delayed by member-state disagreements. In 2022 the US "Sunshine Protection Act" (permanent DST) passed the Senate but stalled in the House. Turkey, Russia, Iceland, China, etc. have already abolished it.

From the developer's perspective: abolition decisions get finalized suddenly, just months in advance, so you must always keep the tzdata update chain alive.


7. Language-Specific datetime APIs

7.1 JavaScript Date — Infamous Design

In 1995 Netscape quickly copied Java's java.util.Date, and that Java API was later deprecated (replaced by Joda Time / java.time). JS, however, couldn't fix it for decades due to compatibility.

Problems:

  • Mutable: date.setMonth(3) modifies the original
  • Months start at 0: new Date(2026, 0, 15) is January 15, 2026
  • Inconsistent string parsing: new Date("2024-03-15") is UTC, new Date("2024/03/15") is local — varies by browser
  • No time-zone handling: only "local" or "UTC." No arbitrary IANA zone support
  • ms precision: no nanoseconds

Libraries like Moment.js, date-fns, Luxon, Day.js filled the gap.

7.2 Temporal — The Savior of 2025

Stage 3 → late 2024 rolled out to browsers/Node. Main types:

  • Temporal.Instant: nanosecond-precision UTC instant
  • Temporal.ZonedDateTime: Instant + IANA time zone
  • Temporal.PlainDate / PlainTime / PlainDateTime: civil without zone
  • Temporal.Duration: duration
  • Temporal.Calendar: Gregorian, Islamic, Japanese, Buddhist, etc.
  • Temporal.Now: Temporal.Now.instant() and friends
const now = Temporal.Now.zonedDateTimeISO('Asia/Seoul')
const oneHourLater = now.add({ hours: 1 })
const duration = now.until(oneHourLater) // Temporal.Duration

Why it's revolutionary:

  • Immutable: every operation returns a new object
  • Explicit time-zone handling: disambiguation: 'reject' | 'earlier' | 'later' | 'compatible'
  • Nanosecond support
  • Correctly handles leap days/years and various calendars

7.3 Python

  • datetime: standard, pass tzinfo directly or use the zoneinfo module (Python 3.9+)
  • pytz (legacy): notorious for astimezone() bugs and the need to call normalize()
  • arrow: friendlier API
  • pendulum: Ruby-style DSL
  • Recommendation: datetime + zoneinfo (stdlib)
from datetime import datetime
from zoneinfo import ZoneInfo
dt = datetime(2026, 4, 15, 13, 30, tzinfo=ZoneInfo("Asia/Seoul"))
dt.astimezone(ZoneInfo("America/New_York"))

7.4 Java

  • java.util.Date: legacy, do not use
  • java.util.Calendar: legacy
  • java.time (Java 8, JSR-310 / based on Joda): modern
    • Instant, LocalDateTime, ZonedDateTime, OffsetDateTime
    • Immutable, thread-safe
  • Joda Time: pre-Java 7 generation. On Java 8+ use java.time.

7.5 Go

  • time.Time stores both a wall clock and a monotonic clock internally
  • Time zones via time.LoadLocation("Asia/Seoul")
  • Peculiar format string: 2006-01-02 15:04:05 -0700 (reference date)
  • Parse: time.Parse(layout, s)

7.6 Rust

  • std::time::SystemTime: OS clock
  • std::time::Instant: monotonic (for measuring elapsed time)
  • Civil/zoned handling via chrono or jiff (announced by BurntSushi in 2024)
  • jiff is getting attention for its Temporal-inspired API

8. Monotonic Clock vs Wall Clock

8.1 The Difference

  • Wall clock: calendar time. May jump forward/back with NTP sync
  • Monotonic clock: close to "time since boot." Never goes backward

8.2 Why It Matters

For measuring elapsed time, always use monotonic:

// Bad — if NTP jumps back, this goes negative
const start = Date.now()
await doWork()
const elapsed = Date.now() - start

// Good
const start = performance.now()
await doWork()
const elapsed = performance.now() - start

Timeouts, rate limiters, benchmarks should all use monotonic clocks.

8.3 Accessing Monotonic by Language

  • JS browser: performance.now() (ms with fractional, since page load)
  • JS Node: process.hrtime.bigint() (ns)
  • Go: time.Now() includes monotonic; time.Since(start) is always safe
  • Java: System.nanoTime()
  • Python: time.monotonic(), time.monotonic_ns()

9. NTP, PTP — Clock Sync in Distributed Systems

9.1 NTP (Network Time Protocol)

  • Layered "stratum" structure:
    • Stratum 0: atomic clocks, GPS receivers
    • Stratum 1: servers directly connected to stratum 0 (NIST, KRISS)
    • Stratum 2: public servers drawing from stratum 1 (time.google.com, time.cloudflare.com)
    • Stratum 3+: general servers
  • UDP port 123
  • Accuracy: a few ms within a LAN, 10–50 ms over the internet
  • Included by default in most OSes (chronyd, systemd-timesyncd, w32time)

9.2 PTP (Precision Time Protocol, IEEE 1588)

  • Nanosecond-to-microsecond accuracy within a LAN
  • Switches must support hardware timestamping
  • Required for financial high-frequency trading and 5G base-station sync
  • AWS released "Time Sync Service" (PTP-based, microsecond accuracy) in 2023

9.3 Google TrueTime

Built for Spanner DB to perform globally consistent transactions. Treats time not as a single value but as an [earliest, latest] interval.

  • GPS + atomic clocks in every data center
  • TT.now() returns {earliest, latest} (typically a 7 ms window)
  • At commit, take latest as the timestamp and wait until latest has passed → guaranteed global ordering

Similar idea: CockroachDB's HLC (Hybrid Logical Clock) — combines NTP with a logical clock.

9.4 Practical Lessons

  • Servers without NTP or with heavy drift must be excluded from distributed systems (unreliable timestamps)
  • Time-based tokens (JWT exp) and rate limiters need 5–10 seconds of leeway to tolerate stratification errors
  • Kubernetes nodes should all use the same NTP server

10. Calendar Systems — Gregorian Isn't Everything

10.1 Major Calendars

  • Gregorian: introduced in 1582, current international standard
  • Julian: pre-Gregorian. Some Orthodox churches still use it
  • Islamic (Hijri): lunar, about 354 days per year. Essential for religious scheduling
  • Hebrew: luni-solar, with leap months
  • Japanese: Gregorian + era name (Reiwa, since 2019). APIs must handle era transitions
  • Thai Buddhist: Gregorian + 543-year offset → 2025 = Buddhist year 2568
  • Persian: official in Iran
  • Chinese: luni-solar with 24 solar terms. The official calendar is Gregorian but traditional holidays (Spring Festival) follow the lunar calendar

10.2 Unicode CLDR / ICU

Multilingual calendar support:

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { year: 'numeric', month: 'long', day: 'numeric' })
  .format(new Date('2026-04-15'))
// "令和8年4月15日"

new Intl.DateTimeFormat('ko-KR-u-ca-buddhist').format(new Date('2026-04-15'))
// "불기 2569년"

10.3 Leap Year Rule (Gregorian)

  • Divisible by 4 → leap
  • Divisible by 100 → common year
  • Divisible by 400 → leap year

2000 was leap (400); 1900 and 2100 are common (100).

"Annual February 29 meeting" occurs only once every four years. Outlook and others offer fallback options like "last day of previous month" or "first day of next month."

10.4 The September 3–13, 1752 Gap

When Britain switched from the Julian to the Gregorian calendar in 1752, September 2 was followed by September 14. Russia did this in 1918. Few libraries model this historical gap precisely (Java GregorianCalendar and Noda Time do, in part).


11. Time Handling in Databases

11.1 PostgreSQL

  • timestamp without time zone: civil time. Stored/queried as-is.
  • timestamp with time zone (timestamptz): internally UTC epoch, converted on I/O by the session's TIME ZONE setting.
  • date, time, interval supported
  • Convert via AT TIME ZONE 'Asia/Seoul'

Recommendation: use timestamptz unless you have a specific reason not to.

11.2 MySQL

  • DATETIME: civil time, no time-zone conversion
  • TIMESTAMP: session tz → UTC on store, UTC → session tz on read (automatic). Has the 2038 problem (32-bit).
  • DATE, TIME supported

Caution: TIMESTAMP results depend on the server's time_zone session variable. Recommended: SET time_zone = '+00:00' per connection.

11.3 MongoDB

  • BSON Date is a 64-bit int in milliseconds (UTC epoch)
  • No time zone → you must convert to UTC before storing

11.4 DynamoDB, Redis

  • Store ISO 8601 strings with offset, or epoch ms
  • TTL is Unix timestamp (seconds)

11.5 ClickHouse

  • DateTime, DateTime64 (sub-second precision)
  • Each column can have a time zone (DateTime('Asia/Seoul'))
  • Convert in aggregation queries via toTimeZone()

12. Timestamps in Logs and Metrics

12.1 Principle: Store UTC, Display Local

  • Server logs: all UTC (set TZ=UTC)
  • Dashboards (Grafana, Kibana): display in the user's browser tz
  • Distributed tracing: span timestamps are UTC epoch ns

12.2 Correlation and Clock Skew

With microservices A, B, C where A calls B and B calls C, if each server's clock differs slightly:

  • B's span may appear to start before A's span
  • OpenTelemetry mitigates this by measuring span duration with a monotonic clock and converting to UTC

12.3 Store as ISO 8601 or RFC 3339

2026-04-15T13:30:00.123456789Z

The Z (= UTC) is essential. Strings without a zone are ambiguous to parse.

12.4 Event Streams Like Kafka

Each message has both a producer timestamp (assigned by the producer) and log append time (recorded by the broker). Order by offset; treat time as informational.


13. Ten Real-World Bug Cases

13.1 "Cron scheduled for 2 AM doesn't run on the DST day"

Cause: 02:30 doesn't exist in that time zone. Cron expressions use local time. Fix: run cron in UTC, or set OnCalendar=UTC explicitly.

13.2 "JWT exp expires oddly early"

Cause: server/client clock skew. Fix: 5–30 seconds leeway. Enforce NTP sync.

13.3 "Email 'Received' header is in the future"

Cause: some relay server's clock runs ahead. Fix: ask the infra team to check NTP. Spam filters treat this as a suspicious signal.

13.4 "Birthday alert comes a day early or late"

Cause: stored in UTC, user moved to a different tz. Fix: store birthdays as PlainDate (no time). Map to 00:00 in the user's tz when sending.

13.5 "Displays as 1970-01-01"

Cause: null timestamp treated as 0, then formatted as a Date. Fix: null-check.

13.6 "2038 test fails"

Cause: 32-bit time_t. Fix: verify 64-bit compilation.

13.7 "Duration is negative"

Cause: used the wall clock. NTP corrected backward. Fix: monotonic clock (performance.now, time.monotonic).

13.8 "Event logs are out of order"

Cause: clock skew between servers. Fix: OpenTelemetry Span Link + logical clock.

13.9 "User booked 'today' but it saved as 'tomorrow'"

Cause: browser tz ≠ server tz. Parsing new Date(...) on the server uses server tz. Fix: use Temporal.PlainDate.from(...) in the browser, or ISO string + explicit tz offset.

13.10 "Vacation days compute as 0.96 across a DST transition"

Cause: (endMs - startMs) / (24 * 3600 * 1000) — DST makes a day 23 or 25 hours. Fix: civil-aware operations like ChronoUnit.DAYS.between(zonedStart, zonedEnd).


Storage

  • Distinguish and store "Instant" vs "Civil+TZ" separately
  • Prefer UTC storage; preserve original civil+tz for recurring schedules
  • Use DB timestamptz (on MySQL, pin the connection tz to UTC)
  • Strings as ISO 8601 + time-zone offset
  • Decide whether you need nanosecond precision (extra column if so)

Conversion

  • Store user tz in their profile (IANA name)
  • Convert to user tz only on UI display
  • Don't rely on server local tz (TZ=UTC enforced)

DST

  • Handle "time that doesn't exist" (reject / next valid)
  • Handle "time that appears twice" (earlier / later)
  • Include DST transition days in test data

Synchronization

  • Verify NTP or chrony is running
  • Monitor clock skew (alert above 5 seconds)
  • Use monotonic clock for elapsed time

Libraries

  • JS: Temporal (or Luxon) — avoid raw Date
  • Java: java.time (avoid Date/Calendar)
  • Python: datetime + zoneinfo
  • Go: time (supported, but mind the layout format)
  • Rust: jiff or chrono

Operations

  • Regular tzdata updates
  • Check the tzdata version in Docker images (alpine)
  • Process for applying JVM TZupdater
  • Monitor news about new DST policy changes

15. Archive of Historical Events

15.1 2007 US DST Date Change

The US government moved DST start three weeks earlier. Many Outlooks and Blackberries kept following the old rules and meetings were off by an hour. MS shipped an emergency patch.

15.2 2011 Samoa Time Zone Change

Crossing the International Date Line meant December 30, 2011 did not exist. December 29 was followed by December 31. Aligning with New Zealand trading hours.

15.3 2016 Turkey

DST abolished with a same-day decision. Older Android devices didn't update automatically and people were an hour late to work.

15.4 2018 North Korea

Changed from UTC+8:30 to UTC+9 (as a gesture toward reunification). tzdata updated.

15.5 2023 Death of GMT?

UK government discussed time-zone policy post-Brexit. No change so far.

15.6 High-Frequency Trading in Finance

NASDAQ and others record order sequence with nanosecond accuracy. PTP + shared atomic clocks. A 1-microsecond difference can decide trades worth millions.


Closing — "There Is More Than One Time"

One recurring view from this article: there is no single concept called "time." There are many layers — Instant, Civil time, Zoned time, Monotonic, Logical time — each fit for a different purpose. Most program bugs arise when these layers get mixed up.

Five core principles:

  1. Store in UTC, display in local — and preserve original civil+tz for recurring schedules
  2. Sort and compute in Instant, display in ZonedDateTime
  3. Use monotonic for elapsed time (wall clock can jump)
  4. Use IANA names for time zones (no abbreviations)
  5. Include DST transition days in test cases

Like Unicode, time is an area where the complexity of human society seeps into software. DST is changed by political decisions, holidays shift with religious calendars, Earth's rotation slows so leap seconds are inserted. In a reality where "just use UTC" doesn't solve everything, developers must carry a layered model of time in their heads.

The next article dives into Consensus in distributed systems — Raft/Paxos, the CAP theorem, and eventual consistency. See how the inconsistency of time we saw here entangles with the core problems of distributed systems (Lamport clock, Vector clock, Hybrid Logical Clock, etc.).

Understanding time isn't just a technique for avoiding bugs. It's also a philosophical question about what "happened at the same time" means. As relativity tells us, in a distributed system there is no "global current time" — we only deal with approximations of it.