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

- Name
- Youngju Kim
- @fjvbn20031
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:
- 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."
- 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.
- 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 (JavaScriptnew Date()uses local; Pythonfromisoformatreturns naive)Zvs+00:00— same meaning but string comparison differs- Sub-second precision varies by system (ms vs μs vs ns)
+09vs+0900vs+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
tzdatapackage 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(...)+ZoneRulesProvidercan handleGap 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 instantTemporal.ZonedDateTime: Instant + IANA time zoneTemporal.PlainDate/PlainTime/PlainDateTime: civil without zoneTemporal.Duration: durationTemporal.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, passtzinfodirectly or use thezoneinfomodule (Python 3.9+)pytz(legacy): notorious forastimezone()bugs and the need to callnormalize()arrow: friendlier APIpendulum: 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 usejava.util.Calendar: legacyjava.time(Java 8, JSR-310 / based on Joda): modernInstant,LocalDateTime,ZonedDateTime,OffsetDateTime- Immutable, thread-safe
- Joda Time: pre-Java 7 generation. On Java 8+ use
java.time.
7.5 Go
time.Timestores 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 clockstd::time::Instant: monotonic (for measuring elapsed time)- Civil/zoned handling via
chronoorjiff(announced by BurntSushi in 2024) jiffis 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
latestas the timestamp and wait untillatesthas 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'sTIME ZONEsetting.date,time,intervalsupported- 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 conversionTIMESTAMP: session tz → UTC on store, UTC → session tz on read (automatic). Has the 2038 problem (32-bit).DATE,TIMEsupported
Caution: TIMESTAMP results depend on the server's time_zone session variable. Recommended: SET time_zone = '+00:00' per connection.
11.3 MongoDB
- BSON
Dateis 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).
14. Checklist — Designing Time-Related Systems
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=UTCenforced)
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 rawDate - Java:
java.time(avoid Date/Calendar) - Python:
datetime + zoneinfo - Go:
time(supported, but mind the layout format) - Rust:
jifforchrono
Operations
- Regular
tzdataupdates - 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:
- Store in UTC, display in local — and preserve original civil+tz for recurring schedules
- Sort and compute in Instant, display in ZonedDateTime
- Use monotonic for elapsed time (wall clock can jump)
- Use IANA names for time zones (no abbreviations)
- 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.