Skip to content

필사 모드: Timestamps and Time Zones, Done Right

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction — Why Is Time So Hard

Code that handles time looks easy on the surface. Get the current time, store it, show it on screen — done, apparently. Then one day a bug report arrives. "My booking is off by an hour." "The date on the report is a day behind." "Only certain users have a weird login time." Most of these bugs are the price of handling time naively.

Time is hard because it is not a pure physical quantity but a concept layered with human conventions. The Earth's rotation, the political boundaries of standard time, daylight saving time, leap seconds, historical offset changes. Behind what we casually call "just time" lies a surprisingly complex set of rules.

Fortunately, there are solid principles that tame this complexity. This post lays them out one by one. Summed up in a single line: store in UTC, display in local. The rest is the story of why and how to keep that principle. If you want to see the concepts in action, it helps to read this with the Unix Timestamp Converter, the UTC Converter, and the World Clock open alongside.

The Golden Rule — Store in UTC, Display in Local

The most important rule in handling time is to separate storage from display.

  • Storage: always store in UTC (Coordinated Universal Time). UTC is a single baseline unshaken by region or daylight saving.
  • Display: convert to the user's time zone only when showing it to them.

Why must storage be in UTC? If you store a local time like "Seoul time 2026-06-14 15:00" as-is, that value cannot stand on its own. In a region with DST, the same wall-clock time can appear twice a year or not exist at all, and if server and client are in different regions, interpretations diverge. Storing in UTC dissolves all this ambiguity. A specific UTC instant points to exactly one absolute moment, everywhere on Earth.

Display is the reverse. Users do not want UTC. A Seoul user wants Korean time; a New York user wants Eastern time. So you convert the stored UTC to the user's time zone only when painting it to the screen. The storage layer holds one truth; the display layer holds many localized views.

Epoch vs ISO 8601 — Two Representations

When we say we store time in UTC, in what format do we actually store it? There are broadly two approaches.

Unix timestamp (epoch). The number of seconds (or milliseconds) elapsed since 1970-01-01 00:00:00 UTC. It is a single integer, like 1749884400.

1749884400  →  2025-06-14T07:00:00Z (UTC)

The advantages are clear. It is a pure absolute moment with no time zone at all. Comparison and arithmetic reduce to integer operations, and it takes little storage. The downside is that humans cannot read it.

ISO 8601 string. A human-readable standard format like 2025-06-14T07:00:00Z. The trailing Z means "Zulu," that is, UTC. To make an offset explicit, you write 2025-06-14T16:00:00+09:00.

2025-06-14T07:00:00Z         ← UTC (Z = +00:00)
2025-06-14T16:00:00+09:00    ← the same moment written in Seoul's offset

Both strings point to the same absolute moment. ISO 8601's advantages are readability and an explicit offset. For logs, API responses, and human-readable data, ISO 8601 is good; for internal computation and storage efficiency, epoch is convenient. What matters is that either way you do not lose the time zone information. A "naive" string like 2025-06-14 16:00:00, with neither an offset nor a Z, is ambiguous in itself and a regular cause of time bugs.

Offset ≠ Time Zone — The Most Common Misconception

Here comes the most important conceptual distinction in handling time. An offset and a time zone are not the same thing.

  • Offset: the difference from UTC. A simple number like +09:00 or -05:00. It is merely the state at a specific moment.
  • Time zone: a set of rules for how a region decides its time. Identified by names like Asia/Seoul or America/New_York.

Why does this distinction matter? Because the offset changes over time. America/New_York is -05:00 (EST) in winter but -04:00 (EDT) in summer because of daylight saving. In other words, fixing "New York time" to a single offset is wrong. The offset varies moment to moment, and what holds the rules for that variation is the time zone.

The time zone America/New_York:
  2025-01-15  →  offset -05:00 (EST, winter)
  2025-07-15  →  offset -04:00 (EDT, summer, DST)

The practical implications are big. When you store a future meeting, you must not store only the offset. Just because Seoul is +09:00 now does not mean you should freeze a meeting six months out at +09:00; if the country changes its DST policy in the meantime, the meeting time drifts. Future events must be stored with the time zone name (Asia/Seoul), so that even if the rules change, the correct wall-clock time is recomputed.

The IANA tz Database — The World's Dictionary of Time Rules

If a time zone is a set of rules, where do those rules live? In the IANA Time Zone Database (the tz database, a.k.a. zoneinfo, the Olson database). It is a continually updated public database that holds the time rules for every region on Earth.

What this database holds is astonishingly vast. Not just current offsets and DST rules, but historical changes too. For example, it records when a country adopted and then abolished DST, and when it changed its standard time offset itself. So if you compute some moment in 1988 with Asia/Seoul, it even reflects the historical fact that Seoul had DST that year.

The key lesson is this: do not hardcode time zone rules yourself. Baking "Korea is UTC+9" into your code is correct now but does not guarantee the future, and it is already wrong for regions with DST. Instead, use libraries backed by the IANA database provided by your OS and language runtime, and keep that database up to date. Countries change their time policies more often than you would think, and each time the tz database is updated.

The Hell of DST — Gaps and Overlaps

Daylight saving time (DST) is the most fertile soil for time bugs. The heart of the problem is that at a DST transition, wall-clock time is not continuous.

Spring — the disappearing hour (gap). When DST begins, the clock jumps forward one hour. For example, the moment it becomes 2:00 a.m., it immediately becomes 3:00 a.m. As a result, the time 2:30 a.m. does not exist in that region that day.

Spring transition (DST starts):
  01:59:59  →  03:00:00   (the whole 2 o'clock hour disappears)
  "02:30" is a time that does not exist that day

If a user set an alarm to fire at 02:30 every day, when should that alarm fire on the day DST starts? This is an ambiguous situation with no single right answer, and different libraries and policies handle it differently.

Autumn — the overlapping hour (overlap). When DST ends, the clock falls back one hour. As a result, the wall-clock time 1:30 a.m. appears twice that day.

Autumn transition (DST ends):
  01:59:59 (DST)  →  01:00:00 (standard)   (the 1 o'clock hour repeats)
  "01:30" is a time that exists twice that day

Now a log saying "something happened at 01:30" is ambiguous about which of the two 01:30s it means. Without an offset, you cannot tell the two moments apart. These two phenomena (gap and overlap) are exactly the decisive reason not to store local time as-is. Storing in UTC means such ambiguity never arises in the first place, because UTC has no DST.

A True Story — A Batch Job That Ran at the DST Transition

A payment system ran a settlement batch job every day at 2:30 a.m. It was based on local time. Normally there was no problem, but on the spring DST transition day, when that time ceased to exist, the batch did not run that day. A day's worth of settlement was missed, and nobody saw an error log — from the scheduler's point of view, "that time simply never came."

There are two lessons. First, recurring schedules are safer if they avoid the DST transition window (usually 1–3 a.m.). Second, you must always check whether your scheduler uses local time or UTC. Many incidents come from "a batch that runs in the small hours on local time."

Leap Seconds — When a Minute Becomes 61 Seconds

Another myth about time is that "a minute is always 60 seconds." Not true. The Earth's rotation is not perfectly regular, so a tiny discrepancy accumulates between atomic-clock time and rotation-based time. To reconcile it, a leap second is occasionally inserted. On such a day, the last minute of the day has 61 seconds.

When a leap second is inserted:
  23:59:58
  23:59:59
  23:59:60   ← a time that exists! (the 60-second mark, absent normally)
  00:00:00

Most applications never handle leap seconds directly, because Unix time itself is defined to "ignore" leap seconds. But leap seconds have caused real incidents. In the past, several large systems had outages when their clocks tangled at the moment a leap second was inserted. Recently, because of this, "leap smearing" — smoothly smearing the leap second across the system — is widely used: slowing the clock by a tiny amount over a whole day to avoid a 61-second minute. The point is to know that the assumption "a second is always a second and a minute is always 60 seconds" can be physically wrong.

Do Not Trust the Client Clock

There is a principle you must engrave in any distributed system: do not trust the client's clock. The user's device clock may be wrong. The battery may have drained and it just booted, the user may have changed it deliberately, or it may simply be off by minutes.

There are many situations where this bites.

  • Security token expiry: if you judge expiry by the client clock, a user can dodge expiry by manipulating the clock. Expiry must be judged by the server's time.
  • Event ordering: if you sort events from multiple devices by client timestamp, a device with a skewed clock tangles the order.
  • First-come-first-served: judging logic like coupon first-come by the client's time is not fair.

The authoritative time is set by the server. Servers align their clocks with each other via NTP (Network Time Protocol), but that is not perfect either, so when precise ordering is needed, use separate mechanisms like a monotonically increasing sequence or a logical clock. And remember that even a server's clock can occasionally jump backward due to NTP synchronization.

NTP and Clocks That Rewind — Time Does Not Only Move Forward

As just noted, clocks do not always move forward. A server aligns to the correct time via NTP, and if its local clock was ahead of reality, NTP adjusts the clock backward. At that moment, from the code's point of view, time appears to flow in reverse.

Why is this dangerous? Consider naive code that measures elapsed time.

start = now()          // wall-clock time
... do work ...
elapsed = now() - start

If NTP pulls the clock back in the meantime, elapsed can go negative. In timeout logic, this produces a bug where it either expires immediately or never expires. That is why you must use a monotonic clock, not the wall clock, to measure elapsed time. A monotonic clock never goes backward and only measures elapsed amount. The rule is: use the wall clock for "what time is it now" and the monotonic clock for "how much time has passed."

Date vs Instant — A Birthday Has No Time Zone

Another important distinction is that a "date" and an "instant" are different kinds of value.

  • Instant: a point on the timeline. Like "2025-06-14T07:00:00Z," it is absolute and common worldwide. An event's occurrence time and a log timestamp belong here.
  • Date-only (civil date): a calendar day independent of time zone. A birthday, a public holiday, a contract expiry date. "June 14" is the same June 14 in Seoul or New York.

Confusing the two produces bugs. If you store a birthday as an instant (say 2000-06-14T00:00:00Z), it can shift by a day during DST or time zone conversion — the classic "birthday off by a day" bug. If the user is in a western time zone, UTC midnight is the previous day there.

The principle is this: when handling a specific moment, store a UTC instant; when handling a calendar date, store a time zone-free date type (LocalDate, date, and so on). You must first ask "is this an absolute moment, or a calendar day?" to pick the right type.

A Practical Checklist

Compressing the principles above into practical rules:

  • Store UTC, display local. Keep the database and API internals uniformly on UTC instants.
  • Do not store naive datetimes. A time with neither an offset nor a Z is ambiguous. Always keep the time zone info with it.
  • Store future events with the time zone name. Storing only the offset is fragile against rule changes.
  • Do not hardcode time zone rules. Use libraries backed by the IANA tz database and keep it up to date.
  • Measure elapsed time with a monotonic clock. The wall clock can move backward due to NTP.
  • Do not trust the client clock. Judge expiry, ordering, and fairness by server time.
  • Distinguish dates from instants. Store birthdays and holidays as time zone-free dates.
  • Keep recurring schedules out of the DST transition window. 1–3 a.m. is the danger zone.

Conclusion

Handling time means handling the point where physics and human convention intersect. That is why naive intuition so often betrays us. That a day is always 24 hours, that a minute is always 60 seconds, that a clock only moves forward, that a time zone is a fixed offset — none of these were true.

But the principles to hold onto in the face of this complexity are surprisingly simple. Store in UTC, display in local, store future events with the time zone name, measure elapsed time with a monotonic clock, and do not trust the client clock. Keep these, and you prevent the vast majority of time-related bugs. If you want to touch the concepts yourself, see how values convert in the Unix Timestamp Converter, the UTC Converter, and the World Clock.

References

현재 단락 (1/81)

Code that handles time looks easy on the surface. Get the current time, store it, show it on screen ...

작성 글자: 0원문 글자: 12,075작성 단락: 0/81