TT Lab
Get started
Learn Learning paths Courses

The Logs Came From the Future — Five Incidents a Clock Made

One in the morning came twice

Continue in TT Lab

One-line summary

Store instants (UTC) and use local time only for display. Records stored in local time overlap or disappear twice a year.

Why this is needed

"Our service is used only in Korea, so we just stored local time." This sentence causes no trouble for years. Korea does not currently use daylight saving time, so the difference between Korean time and UTC is always fixed at 9 hours. The problem comes afterward. The moment you get US customers and start sending notifications in their local time, add one more cloud region, or switch the settlement basis to local business days, this design breaks. And since the days it breaks are only twice a year, ordinary testing never catches it.

How it works

The rules for converting between local time and an instant are set politically by each country, and the collection of that history is the IANA time zone database. Names such as Asia/Seoul and America/New_York are not city names but the names of the entire set of time rules of that region. This database is updated whenever a political decision is made. The latest release at the time of checking was 2026d (published 2026-09-11), and one of the changes in that release was the Northwest Territories of Canada moving to a permanent -06. Because the rules keep changing like this, you must not hard-code offsets as numbers such as +09:00 in code. Store the name and leave the conversion to the database.

On the day daylight saving time ends, clocks are turned back by one hour. Then that hour of local time occurs twice. On the day it starts, clocks are turned forward by one hour, so that hour does not exist at all. To tell these two cases apart, Python gave datetime an attribute called fold. PEP 495 is the proposal for it: 0 is the earlier of the two candidates and 1 is the later.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

ny = ZoneInfo("America/New_York")

def classify(naive):
    early = naive.replace(tzinfo=ny, fold=0)
    late = naive.replace(tzinfo=ny, fold=1)
    if early.utcoffset() == late.utcoffset():
        return "normal"          # 후보가 하나뿐 — 평범한 시각
    back = early.astimezone(timezone.utc).astimezone(ny).replace(tzinfo=None)
    return "ambiguous" if back == naive else "nonexistent"

classify(datetime(2025, 11, 2, 1, 30))   # ambiguous  — 두 번 온다
classify(datetime(2025, 3, 9, 2, 30))    # nonexistent — 오지 않는다

The latter part of the judgment is the key. The fact that the two candidates' offsets differ does not by itself separate a "time that occurs twice" from a "time that does not exist" — in both cases the offsets differ. The distinction is made with a round trip. Convert the local time to an instant and back to local time; if the original value comes out, the time is real (the occurs-twice case), and if a different value comes out, the time never existed.

The storage rule then becomes simple. Store instants in UTC. This is why, among PostgreSQL's date/time types, you are told to use timestamptz. The name is confusing, but this type does not store a time zone — it converts the input to UTC for storage and shows it in the session's time zone when read. By contrast, timestamp (without time zone) holds the written number as is, so nobody knows which region that number belongs to.

The exception, however, is future appointments. "A meeting at 9 a.m. on the first Monday of next month" is an appointment made in local time, not an instant. If that country changes its daylight saving rules in the meantime, a value fixed in UTC fires at the wrong time. For these, store the local time and the time zone name together and convert when reading.

What you see in the field

The most common accident is a batch job named after local time. In the early morning when daylight saving time ends, the "01:00 settlement" runs twice. Both runs finish normally, the logs show no errors, and the total number of runs is the same as usual. It is just that that day's sales get counted twice.

The second most common is a schedule placed on a nonexistent time. The 2 a.m. hour on the morning daylight saving time starts does not exist in that region, so a job scheduled for that time is quietly skipped once. It did not fail; it simply never happened, so no failure alert arrives either. Nobody knows until someone says the next morning, "there's no report from yesterday."

The third is human misjudgment. Looking at records kept only in local time, someone asks, "there are two entries at 01:30 — are they duplicates?" Those two are in fact different instants, an hour apart. If the records have no offset, there is in principle no way to tell.

What to check in the next quiz

Check which two accidents happen when you store in local time, how fold separates the two cases, and what exceptions there are to the rule of storing in UTC.