Skip to content

Split View: 시간과 날짜의 기술적 지옥 완전 해설 — TimeZone, DST, 윤초, Temporal API, NTP 끝장 가이드 (2025)

|

시간과 날짜의 기술적 지옥 완전 해설 — TimeZone, DST, 윤초, Temporal API, NTP 끝장 가이드 (2025)

들어가며 — "2024년 10월 27일 02:30"이 존재하지 않을 수 있다

유럽에서 서머타임이 끝나는 날, 새벽 03:00이 되면 시계가 02:00으로 되돌아간다. 즉, 같은 날 02:30이 두 번 나온다. 그날 02:30에 쿠폰을 발급한 이벤트가 있다면, 그 쿠폰은 어느 02:30의 것인가?

반대로, 3월 마지막 일요일 유럽은 02:00에서 03:00으로 건너뛴다. 02:30이라는 시각 자체가 존재하지 않는다. 사용자가 "02:30에 예약"이라고 저장하면 그 예약은 언제 실행될까?

이런 질문들은 단순한 UX 이슈가 아니다. 금융 거래의 order 정렬, 로그의 시간순 분석, 분산 시스템의 이벤트 ordering, 임금 계산 같은 실전 시스템이 이 미묘함에 깨진다. Jon Skeet(Noda Time 저자)의 유명한 말: "Time is a joke. Seriously."

이 글은 시간과 날짜를 다루는 기술적 기반을 처음부터 설명한다. UTC가 뭐고 GMT와 어떻게 다른지, Unix time은 왜 윤초를 무시하는지, IANA 타임존 데이터베이스가 왜 자주 업데이트되는지, DST 전환을 어떻게 안전하게 처리하는지, new Date()의 악명 높은 함정들, JavaScript Temporal API가 왜 10년 만의 혁신인지, 분산 시스템에서 NTP/PTP로 시계를 맞추는 방법, 그리고 Google Spanner의 TrueTime까지.

Unicode 글에서 "문자열은 어려운 문제다"라고 했다면, 시간은 그보다 더 어렵다. 문자열은 적어도 한 컴퓨터 안에서 결정론적이지만, 시간은 지구의 자전 속도 변화, 국가의 정치적 결정, OS의 clock drift까지 얽혀 있다.


1. 시간의 3가지 개념 — Instant, Local, Civil

시간을 프로그램에서 다룰 때 먼저 구분해야 할 세 가지:

1.1 Instant (순간)

"지금 이 순간"이라는 절대적 시점. 지구 어느 곳에서 관측하든 같은 값이다. Unix timestamp(1970-01-01T00:00:00Z부터 경과한 초)가 대표적 표현이다.

예: 1760000000 (2025-10-09T07:33:20Z에 해당하는 초 단위 epoch)

1.2 Civil Date/Time (민간 시각)

"2026년 4월 15일 13:30"처럼 달력과 시계에서 읽는 값. 타임존 없이는 모호하다.

1.3 Zoned Date/Time (타임존이 붙은 민간 시각)

"2026-04-15 13:30 Asia/Seoul". 이제 Instant로 변환 가능하다.

1.4 핵심 규칙

  • DB/로그/정렬: Instant로 저장 (UTC 시점)
  • UI 표시: 사용자 타임존으로 Zoned로 변환
  • 반복 일정("매월 1일 9시")은 Civil + TimeZone으로 저장

"UTC로 저장하면 된다"는 말은 반은 맞고 반은 틀리다. "2026년 12월 25일 00:00에 알림"이라는 일정을 서울 사용자가 세팅했다면, UTC로 환산해 저장하면 DST 변화/타임존 변경 시 날짜가 바뀐다. 이런 경우는 원본 civil+tz를 보존해야 한다.


2. UTC, GMT, TAI, UT1 — 시간 표준의 혼란

2.1 GMT (Greenwich Mean Time)

영국 그리니치의 평균 태양시. 19세기 선박 항해를 위해 표준화. 법적으로는 영국의 일부 공식 문서에서 여전히 쓰이지만, 기술 문서에서 "GMT"는 거의 항상 UTC의 구어적 동의어로 쓰인다.

2.2 UTC (Coordinated Universal Time)

국제 표준. 원자시(atomic time) 기반으로 매우 정밀하지만, 지구 자전과 맞추기 위해 **윤초(leap second)**를 삽입한다. UTC = TAI - (누적 윤초 수).

2.3 TAI (International Atomic Time)

전 세계 약 400개 원자시계의 평균. 윤초 없음. 2025년 기준 UTC보다 37초 앞서 있다. GPS time은 UTC가 아니라 TAI 기반이고 1980년 기준 오프셋을 가진다.

2.4 UT1

지구 자전에 실제로 맞춘 시간. UTC는 UT1과 0.9초 이상 차이 나면 안 된다는 규약으로 윤초가 추가된다.

2.5 실전 정리

  • 프로그램에서 "UTC"라 부르는 것: 대부분 UTC = epoch + 초 (윤초 무시)
  • 엄밀한 UTC: 윤초 있음
  • 보통 사용에서는 차이가 무시 가능하지만, 금융 High-Frequency Trading, 통신 위성 동기화 등에서는 중요

3. Unix Timestamp — 단순함의 대가

3.1 정의

1970-01-01T00:00:00Z부터 경과한 초(또는 밀리초/나노초).

  • POSIX 표준: 초 단위, 윤초 무시
  • 즉, 하루는 정확히 86,400초로 취급

3.2 윤초는 어디로 사라지는가

실제 지구 시간에서 윤초가 삽입되면 UTC는 23:59:60을 한 번 거친다. Unix time은 이를 어떻게 처리하나?

두 가지 방식:

  1. Slew (권장): 윤초 전후 수 시간에 걸쳐 시간을 천천히 늘려서 흡수. Google/AWS/Meta가 쓰는 방식. 어떤 순간도 "건너뛰지" 않음.
  2. Smear: NTP 서버가 윤초 당일 86,400초가 아닌 86,401초 수준으로 slew. 실제 OS의 clock도 부드럽게 진행.
  3. Jump: 23:59:59에서 00:00:00으로 다시 점프. 많은 시스템이 crash하거나 이중 이벤트 생성.

역사적 사고:

  • 2012년 6월 30일 윤초: Linux 커널 버그로 Java 애플리케이션 다수가 CPU 100% 점유. Reddit, LinkedIn, Foursquare 다운
  • 2015년 6월 30일 윤초: Twitter/Instagram 일시 장애
  • 2017년 1월 1일 윤초: Cloudflare DNS가 윤초 계산 오류로 일부 장애

미래: 2022년 국제도량형총회(CGPM)가 2035년까지 윤초 폐지를 결정. 그 이후는 수십 년 단위로 "윤분" 등으로 큰 단위 조정.

3.3 2038년 문제

Unix time을 32비트 signed integer로 저장하면 2038-01-19T03:14:07Z 이후 오버플로우. 32비트 임베디드 시스템(IoT, 구식 데이터베이스 포맷)의 시한폭탄.

대응: 대부분 현대 OS/언어는 64비트로 전환했지만, 레거시 C/C++ 바이너리, 일부 DB 스키마, 파일 포맷을 점검해야 한다. time_t를 명시적으로 size 확인.

3.4 0년 문제 vs 음수 epoch

-62135596800은 서기 1년 1월 1일 UTC. 더 이전 날짜(기원전)는 protocol에 따라 표현 가능/불가능. Java Instant.MIN은 -9999999-01-01, Instant.MAX는 9999999-12-31.


4. ISO 8601 — 문자열 표현 표준

4.1 기본 형식

2026-04-15                       날짜
2026-04-15T13:30:00              로컬 시각
2026-04-15T13:30:00Z             UTC
2026-04-15T13:30:00+09:00        서울 시각
2026-04-15T13:30:00.123456789Z   나노초 정밀도
2026-W16-3                       주 기반 (ISO 주 16의 수요일)
P1Y2M10DT2H30M                   기간 (1년 2개월 10일 2시간 30분)
2026-04-15/2026-04-20            간격

4.2 RFC 3339 — ISO 8601의 subset

IETF가 인터넷 프로토콜용으로 제한한 subset. T 소문자 허용, 타임존 오프셋 필수 등.

4.3 일상의 함정

  • 2026-04-15T13:30:00 (타임존 없음) → 로컬? UTC? 파서마다 다름 (JavaScript new Date()는 로컬, Python fromisoformat는 naive)
  • Z vs +00:00 — 의미는 같으나 문자열 비교는 다름
  • 소수점 이하 초의 정밀도가 시스템마다 다름 (ms vs μs vs ns)
  • +09 vs +0900 vs +09:00 — 파서 지원 범위 차이

교훈: 문자열로 주고받을 때 반드시 타임존 오프셋 명시. 파서는 ISO 8601 공식 라이브러리(Java Instant.parse, Python 3.11+ fromisoformat, JS Temporal.Instant.from) 사용.


5. 타임존 — 정치의 시간

5.1 타임존이 하는 일

UTC와의 오프셋을 결정한다. 하지만 오프셋은:

  • 지역마다 다름
  • 역사적으로 변함 (국가가 DST 도입/폐지, 오프셋 변경)
  • 정치적 결정으로 갑자기 바뀔 수 있음

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

1986년부터 Paul Eggert가 유지하는 시간대 역사의 교과서. Asia/Seoul, America/New_York 같은 이름을 키로, 해당 지역이 역사적으로 사용한 모든 오프셋/DST 규칙/변경 이력을 담고 있다.

예: Asia/Seoul은 1954년 3월 21일에 UTC+8:30에서 UTC+9로 변경. 1961년까지 여러 번 오프셋이 바뀌었고, 1987~1988년 DST가 있었다가 폐지. 지금 한국 API가 "1955년 1월 1일 00:00 Seoul"을 UTC로 변환하려면 이 역사를 알아야 한다.

5.3 tzdata 업데이트

매년 5~10회 새 버전 나온다. 최근 변경:

  • 2024년: 이집트 DST 재도입
  • 2023년: 멕시코 대부분 DST 폐지
  • 2022년: 칠레 DST 날짜 변경
  • 2022년: 요르단, 시리아가 영구 DST로 전환

운영에서 중요:

  • 컨테이너 이미지의 tzdata 버전 체크 (alpine은 tzdata 패키지 별도 설치 필요)
  • JVM은 TZupdater로 최신 tzdata 갱신 가능
  • DB 서버 OS의 tzdata가 구식이면 앱이 잘못된 오프셋 사용
  • Node.js는 ICU를 내장하지만 일부 slim 배포판은 small ICU만 → 타임존 제한됨 (NODE_ICU_DATA 설정)

5.4 abbreviation의 모호성

CST — 어느 국가?

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

IST — 더 심함:

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

규칙: 약어는 표시용으로만 사용. 프로그램에서는 **IANA 이름(Asia/Seoul, America/Chicago)**을 써라.

5.5 POSIX TZ 문자열

구식 형식이지만 여전히 살아있다: EST5EDT,M3.2.0,M11.1.0. 절대 쓰지 말고 IANA 이름으로.


6. DST — 실전 지뢰

6.1 "존재하지 않는 시간"

2024년 3월 31일 유럽, 02:00 → 03:00으로 점프. 02:30은 존재하지 않는다.

// JavaScript 잘 모르는 경우
new Date('2024-03-31T02:30:00+01:00').toISOString()
// "2024-03-31T01:30:00.000Z" — 실제로는 의도한 02:30이 아님

대응:

  • 사용자 입력의 civil time이 존재하지 않으면 에러 또는 forward shift(다음 유효 시간으로)
  • Java ZonedDateTime.of(...) + ZoneRulesProviderGap 처리 가능
  • Temporal.ZonedDateTime.from({..., disambiguation: 'earlier'/'later'/'reject'/'compatible'})

6.2 "두 번 나타나는 시간"

2024년 10월 27일 유럽, 03:00 → 02:00으로 되돌아감. 02:30은 두 번 존재한다.

  • 같은 "2024-10-27 02:30 Europe/Berlin" 문자열이 두 개의 UTC 시점에 해당
  • 금융 로그는 UTC 기준으로 정렬해야 안전

6.3 달력 예약의 재앙

"매주 월요일 09:00 미팅"을 UTC로 변환해서 저장했다면:

  • 서울(항상 UTC+9)은 UTC 00:00
  • 뉴욕(DST 있음)은 UTC 14:00 ↔ UTC 13:00 (여름/겨울 다름)

원본 civil+tz로 저장하는 게 정답. DB 컬럼을 (timestamp, timezone, recurrence_rule)로 분리.

6.4 DST 폐지 추세

EU는 2019년 "2021년부터 DST 폐지" 결정했으나 각국 이견으로 연기. 미국은 2022년 "Sunshine Protection Act"로 영구 DST 법안이 상원 통과했으나 하원 계류. 터키/러시아/아이슬란드/중국 등은 이미 폐지.

개발자 관점: 폐지 결정은 수개월 전에 갑자기 확정되므로 tzdata 업데이트 체인을 항상 유지해야 한다.


7. 언어별 datetime API

7.1 JavaScript의 Date — 악명 높은 설계

1995년 Netscape가 Java java.util.Date를 빠르게 베꼈는데, 그 Java API는 이후 폐기됐다(Joda Time/java.time으로 교체). 하지만 JS는 수십 년간 호환성 때문에 고치지 못했다.

문제점:

  • 가변(mutable): date.setMonth(3)이 원본을 바꿈
  • 월이 0부터 시작: new Date(2026, 0, 15) = 2026년 1월 15일
  • 문자열 파싱 비일관: new Date("2024-03-15")는 UTC, new Date("2024/03/15")는 로컬 — 브라우저마다 다름
  • 타임존 처리 불가: "로컬" 또는 "UTC" 둘 중 하나. 임의 IANA zone 지원 없음
  • 정밀도 ms: 나노초 불가

Moment.js, date-fns, Luxon, Day.js 같은 라이브러리가 이 갭을 메웠다.

7.2 Temporal — 2025년의 구세주

Stage 3 → 2024년 말 브라우저/Node 순차 구현. 주요 타입:

  • Temporal.Instant: 나노초 정밀도 UTC 시점
  • Temporal.ZonedDateTime: Instant + IANA timezone
  • Temporal.PlainDate / PlainTime / PlainDateTime: 타임존 없는 civil
  • Temporal.Duration: 기간
  • Temporal.Calendar: 그레고리력/이슬람력/일본력/불기 등
  • Temporal.Now: Temporal.Now.instant()
const now = Temporal.Now.zonedDateTimeISO('Asia/Seoul')
const oneHourLater = now.add({ hours: 1 })
const duration = now.until(oneHourLater) // Temporal.Duration

혁신 포인트:

  • 불변(immutable): 모든 연산이 새 객체 반환
  • 명시적 타임존 처리: disambiguation: 'reject' | 'earlier' | 'later' | 'compatible'
  • 나노초 지원
  • 윤일/윤년/각종 달력 정확히 처리

7.3 Python

  • datetime: 표준, tzinfo를 직접 넣거나 zoneinfo 모듈(Python 3.9+) 사용
  • pytz (구식): astimezone() 버그와 normalize() 필요로 악명
  • arrow: 더 친숙한 API
  • pendulum: Ruby-style DSL
  • 권장: 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: 구식, 쓰지 말 것
  • java.util.Calendar: 구식
  • java.time (Java 8, JSR-310 / Joda 기반): 현대
    • Instant, LocalDateTime, ZonedDateTime, OffsetDateTime
    • 불변, 스레드 안전
  • Joda Time: Java 7 이전 세대. Java 8+에서는 java.time 사용.

7.5 Go

  • time.Time 내부에 wall clock + monotonic clock 둘 다 저장
  • 타임존은 time.LoadLocation("Asia/Seoul")
  • 포맷 문자열이 독특: 2006-01-02 15:04:05 -0700 (참조 날짜)
  • 파싱: time.Parse(layout, s)

7.6 Rust

  • std::time::SystemTime: OS 시계
  • std::time::Instant: monotonic (경과 시간 측정용)
  • Civil/zoned 처리는 chrono 또는 jiff (2024년 BurntSushi 발표) crate
  • jiff는 Temporal-inspired API로 주목

8. Monotonic Clock vs Wall Clock

8.1 차이

  • Wall clock: 달력 시간. NTP 동기화로 앞/뒤로 뛸 수 있음
  • Monotonic clock: "부팅 이후 경과 시간"에 가까운 것. 절대 뒤로 안 감

8.2 왜 중요한가

경과 시간 측정에는 반드시 monotonic 사용:

// ❌ NTP sync가 뒤로 뛰면 음수가 나옴
const start = Date.now()
await doWork()
const elapsed = Date.now() - start

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

타임아웃 / rate limiter / 벤치마크 모두 monotonic clock 써야 한다.

8.3 언어별 monotonic 접근

  • JS 브라우저: performance.now() (밀리초 + 소수점, 페이지 로드부터)
  • JS Node: process.hrtime.bigint() (나노초)
  • Go: time.Now()가 monotonic 포함, time.Since(start)는 항상 안전
  • Java: System.nanoTime()
  • Python: time.monotonic(), time.monotonic_ns()

9. NTP, PTP — 분산 시스템의 시계 동기화

9.1 NTP (Network Time Protocol)

  • Layered "stratum" 구조:
    • Stratum 0: 원자시계, GPS 수신기
    • Stratum 1: Stratum 0에 직접 연결된 서버 (NIST, KRISS)
    • Stratum 2: Stratum 1에서 받는 공개 서버 (time.google.com, time.cloudflare.com)
    • Stratum 3+: 일반 서버
  • UDP 123 포트
  • 정확도: LAN 내 수 ms, 인터넷 거리 10~50 ms
  • 대부분의 OS가 기본 탑재 (chronyd, systemd-timesyncd, w32time)

9.2 PTP (Precision Time Protocol, IEEE 1588)

  • LAN 내 나노초~마이크로초 정확도
  • 스위치가 timestamping hardware 지원 필요
  • 금융 High-Frequency Trading, 5G 기지국 동기화에서 필수
  • AWS는 2023년 "Time Sync Service" (PTP 기반, 마이크로초 정확도) 공개

9.3 Google TrueTime

Spanner DB가 전 세계적으로 일관된 트랜잭션을 하려고 만든 시스템. 시간을 단일 값이 아닌 [earliest, latest] 구간으로 다룬다.

  • 각 데이터센터에 GPS + 원자시계 설치
  • TT.now()가 {earliest, latest} 반환 (보통 7ms 폭)
  • 트랜잭션 commit 시 latest를 timestamp로 삼고, latest 경과까지 대기 → 글로벌 ordering 보장

비슷한 아이디어: CockroachDB의 HLC (Hybrid Logical Clock) — NTP + 논리 시계 결합.

9.4 실전 교훈

  • NTP가 없거나 drift가 심한 서버는 분산 시스템에서 배제해야 함 (신뢰할 수 없는 timestamp)
  • 시간 기반 token(JWT exp)이나 rate limiter는 5~10초 leeway를 둬야 stratification 에러에 견딤
  • Kubernetes 노드는 모두 같은 NTP 서버 사용 권장

10. 달력 시스템 — 그레고리력이 전부가 아니다

10.1 주요 달력

  • 그레고리력: 1582년 도입, 현재 국제 표준
  • 율리우스력: 그레고리력 전. 일부 정교회가 여전히 사용
  • 이슬람력(Hijri): 달 기반, 1년 약 354일. 종교적 일정 계산에 필수
  • 히브리력: 달-태양 결합, 윤달 있음
  • 일본력: 그레고리력 + 연호(令和, 2019~). API가 연호 전환에 대응해야 함
  • 태국 불기: 그레고리력 + 543년 오프셋 → 2025년 = 불기 2568년
  • 페르시아력: 이란 공식
  • 중국력: 달-태양, 24절기. 공식 달력은 그레고리이지만 전통 명절(춘절)은 음력

10.2 Unicode CLDR / ICU

다국어 달력 지원:

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 윤년 규칙 (그레고리력)

  • 4로 나눠지면 윤년
  • 100으로 나눠지면 평년
  • 400으로 나눠지면 윤년

2000년은 윤년(400), 1900년/2100년은 평년(100).

"매년 2월 29일 미팅"은 4년에 한 번만 생긴다. Outlook 등은 "이전 달 마지막 날" 또는 "다음 달 1일"로 fallback 옵션을 제공.

10.4 1752년 9월 3~13일의 공백

영국은 1752년에 율리우스력에서 그레고리력으로 전환하면서 9월 2일 다음이 9월 14일이다. 러시아는 1918년. 이 역사적 공백을 정확히 모델링하는 라이브러리는 드물다 (Java GregorianCalendar, Noda Time 등이 일부 지원).


11. 데이터베이스의 시간 처리

11.1 PostgreSQL

  • timestamp without time zone: civil time. 저장/조회 시 그대로.
  • timestamp with time zone (timestamptz): 내부는 UTC epoch, 입출력 시 session의 TIME ZONE 설정으로 변환.
  • date, time, interval 지원
  • AT TIME ZONE 'Asia/Seoul'로 변환

권장: 특별한 이유 없으면 timestamptz 사용.

11.2 MySQL

  • DATETIME: civil time, 타임존 변환 없음
  • TIMESTAMP: 저장 시 세션 tz → UTC, 조회 시 UTC → 세션 tz (자동). 2038 문제 있음 (32비트).
  • DATE, TIME 지원

주의: TIMESTAMP는 서버의 time_zone 세션 변수에 따라 결과가 달라진다. 커넥션마다 SET time_zone = '+00:00' 권장.

11.3 MongoDB

  • BSON Date는 64비트 int milliseconds (UTC epoch)
  • 타임존 없음 → 저장 전 UTC 변환 필수

11.4 DynamoDB, Redis

  • 타임스탬프 문자열(ISO 8601 with offset) 또는 epoch ms 저장
  • TTL은 Unix timestamp(초)

11.5 ClickHouse

  • DateTime, DateTime64(서브초 정밀도)
  • 각 컬럼이 타임존을 가질 수 있음 (DateTime('Asia/Seoul'))
  • 집계 쿼리에서 toTimeZone() 함수로 변환

12. 로그와 메트릭의 timestamp

12.1 원칙: UTC로 저장, 로컬로 표시

  • 서버 로그: 모두 UTC (TZ=UTC 설정)
  • 대시보드(Grafana, Kibana): 사용자 브라우저 tz로 표시
  • 분산 tracing: span의 timestamp는 UTC epoch ns

12.2 Correlation과 Clock Skew

마이크로서비스 A, B, C가 있을 때 A가 B를 호출하고 B가 C를 호출한다. 각 서버의 시계가 약간씩 다르면:

  • B의 span이 A의 span보다 먼저 시작한 것처럼 보일 수 있음
  • OpenTelemetry는 이를 완화하기 위해 span의 duration을 monotonic clock으로 측정 후 UTC로 환산

12.3 ISO 8601 또는 RFC 3339로 저장

2026-04-15T13:30:00.123456789Z

여기서 Z(= UTC)는 필수. TZ 없는 문자열은 파싱이 모호하다.

12.4 Kafka 등 이벤트 스트림

각 메시지는 producer timestamp(producer가 붙인 시각)와 log append time(broker가 저장한 시각) 두 가지를 가진다. 순서 정렬은 offset으로 하고 시각은 참고용.


13. 실전 버그 케이스 10선

13.1 "새벽 2시에 실행될 cron이 DST 날 실행 안 됨"

원인: 새벽 2:30이 존재하지 않는 시간대. cron 표현식은 로컬 시간 기준.
해결: UTC로 cron 돌리거나, OnCalendar=UTC 옵션 명시.

13.2 "JWT exp가 이상하게 빨리 만료됨"

원인: 서버와 클라이언트 시계 차이.
해결: 5~30초 leeway. NTP 동기화 강제.

13.3 "이메일의 'Received' 헤더 시간이 미래"

원인: 어느 중계 서버의 시계가 앞서 있음.
해결: 인프라 팀에 NTP 점검 요청. Spam filter는 이런 경우 의심 신호로 취급.

13.4 "생일 알림이 하루 전/후에 옴"

원인: UTC 기준 저장했는데 사용자가 다른 tz로 이동.
해결: 생일은 PlainDate로 저장 (시각 없음). 알림 발송 시 사용자 tz의 00:00에 매핑.

13.5 "1970-01-01 표시"

원인: null timestamp를 0으로 취급 + Date로 format.
해결: null 체크.

13.6 "2038년 테스트 실패"

원인: 32비트 time_t.
해결: 64비트로 컴파일 확인.

13.7 "duration이 음수"

원인: wall clock 사용. NTP가 뒤로 보정.
해결: monotonic clock (performance.now, time.monotonic).

13.8 "이벤트 로그가 뒤죽박죽"

원인: 서버 간 clock skew.
해결: OpenTelemetry의 Span Link + Logical Clock.

13.9 "사용자가 '오늘' 예약했는데 '내일'로 저장됨"

원인: 브라우저 tz ≠ 서버 tz. 서버에서 new Date(...)로 파싱하면 서버 tz 기준.
해결: 브라우저에서 Temporal.PlainDate.from(...) 또는 ISO 문자열 + tz 오프셋 명시.

13.10 "휴가 일수 계산이 DST 전환 때 0.96일"

원인: (endMs - startMs) / (24 * 3600 * 1000)에서 DST로 하루가 23 or 25시간이 됨.
해결: ChronoUnit.DAYS.between(zonedStart, zonedEnd) 같은 civil-aware 연산.


14. 체크리스트 — 시간 관련 시스템 설계

저장

  • "Instant"와 "Civil+TZ"를 구분해 저장
  • UTC 저장 우선, 반복 일정은 original civil+tz 보존
  • DB timestamptz 사용 (MySQL은 연결 tz UTC 고정)
  • ISO 8601 + timezone offset 문자열
  • 나노초 정밀도 필요한지 확인 (부족하면 부가 컬럼)

변환

  • 사용자 tz를 profile에 저장 (IANA 이름)
  • UI 표시 시에만 사용자 tz로 변환
  • 서버 로컬 tz에 의존하지 않기 (TZ=UTC 강제)

DST

  • "존재하지 않는 시각" 핸들링 (reject / next valid)
  • "두 번 나타나는 시각" 핸들링 (earlier / later)
  • Test data에 DST 전환일 포함

동기화

  • NTP 또는 chrony 구동 확인
  • Clock skew monitoring (5초 이상 alerts)
  • 경과 시간은 monotonic clock

라이브러리

  • JS: Temporal (또는 Luxon) — Date 직접 사용 지양
  • Java: java.time (Date/Calendar 지양)
  • Python: datetime + zoneinfo
  • Go: time (지원하지만 layout 포맷 주의)
  • Rust: jiff 또는 chrono

운영

  • tzdata 정기 업데이트
  • Docker 이미지의 tzdata 버전 확인 (alpine)
  • JVM의 TZupdater 적용 프로세스
  • 신규 DST 정책 변경 뉴스 모니터링

15. 역사적 사건 아카이브

15.1 2007년 미국 DST 날짜 변경

미 정부가 DST 시작을 3주 앞당김. 많은 Outlook/Blackberry가 구 규칙대로 동작해서 미팅 1시간 어긋남. MS는 긴급 패치 배포.

15.2 2011년 사모아 시간대 변경

국제날짜변경선 넘어가면서 2011년 12월 30일이 없었다. 12월 29일 다음이 12월 31일. 뉴질랜드 거래 시간대 맞춤.

15.3 2016년 터키

당일 결정으로 DST 폐지. 구 Android 기기들이 자동 변경 안 되어 1시간 늦게 출근.

15.4 2018년 북한

UTC+8:30 → UTC+9로 변경(남북 통일 제스처). tzdata 업데이트.

15.5 2023년 그리니치 표준시의 죽음?

UK 정부가 브렉시트 후 타임존 정책 논의. 아직 변화 없음.

15.6 금융 High-Frequency Trading

나스닥 등은 나노초 정확도로 주문 순서 기록. PTP + 공유 atomic clock. 1 마이크로초 차이가 수백만 달러 거래 결과를 가름.


마무리 — "시간은 여러 개다"

본 글에서 반복한 관점 하나: "시간"이라는 단일 개념은 없다. Instant, Civil time, Zoned time, Monotonic, Logical time 등 여러 층위의 시간이 있고, 각자 다른 목적에 맞는다. 프로그램의 버그 대부분은 이 층위를 혼동할 때 발생한다.

핵심 원칙 5가지:

  1. 저장은 UTC, 표시는 local — 그리고 반복 일정은 원본 civil+tz 보존
  2. 정렬·계산은 Instant로, UI는 ZonedDateTime으로
  3. 경과 시간은 Monotonic으로 (wall clock은 뛸 수 있다)
  4. 타임존은 IANA 이름으로 (약어 금지)
  5. DST 전환일을 테스트 케이스에 포함

Unicode처럼, 시간도 인간 사회의 복잡성이 소프트웨어에 스며든 영역이다. 정치적 결정으로 DST가 바뀌고, 종교 달력에 따라 휴일이 달라지고, 지구 자전이 느려져서 윤초가 삽입된다. "Just use UTC"로 해결되지 않는 현실에서, 개발자는 시간의 레이어드 모델을 머릿속에 가져야 한다.

다음 글에서는 분산 시스템의 Consensus — Raft/Paxos, CAP 정리, 그리고 eventual consistency를 파고든다. 이번 글에서 본 시간의 불일치가 분산 시스템의 핵심 난제와 어떻게 얽히는지(Lamport clock, Vector clock, Hybrid Logical Clock 등) 이어서 보자.

시간을 이해하는 건 단지 버그를 피하는 기술이 아니다. "동시에 일어났다"가 무엇을 의미하는가에 대한 철학적 질문이기도 하다. 상대성 이론이 말하듯, 분산 시스템에서 "전역 현재 시각"은 존재하지 않는다 — 우리는 그것의 근사치를 다룰 뿐이다.

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

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.