# Dates and Time Zones

> The bug that corrupts data and breaks launches: UTC versus local, offsets versus zones, DST, and the rules that keep time from wrecking your app.


---

# Dates and Time Zones

You've shipped the feature. It works on your machine. Then a user in another country says their appointment shows up an hour off, or the report that should cover "yesterday" is missing the last few rows, or once a year - always around 2am, always in the spring or fall - something silently breaks. Time looks like the easy part. It is not.

Here's the relief: almost every time bug comes from a small number of confusions, and they all have the same cure. Once you can tell a *moment* from how it's *displayed*, an *offset* from a *zone*, and you internalize the golden rules, the whole category of "time is haunted" bugs stops happening to you. You don't need to be clever. You need to stop being clever in the three places where clever is what bites you.

## How to read this
- **Want the one idea that fixes most bugs?** Read [Phase 1](01-a-moment-is-not-a-clock-reading.md). Separating the instant from its display is the whole game.
- **Want it to actually stick?** Read in order. Phase 2 is the rules you follow every day; Phase 3 is the day daylight saving time tries to ruin your life, and why your hand-rolled math can't survive it.

## The phases
1. **[A Moment Is Not a Clock Reading](01-a-moment-is-not-a-clock-reading.md)** - the core split: an instant on the timeline versus the local wall-clock string a human reads. UTC, Unix timestamps, and why "3pm" is not a moment until you say *where*.
2. **[Offsets, Zones, and the Golden Rules](02-offsets-zones-and-the-golden-rules.md)** - why `+02:00` is not the same thing as `Europe/Berlin`, what the IANA database actually is, and the small set of rules - store UTC, convert at the edges, never hand-roll, use a real library - that keep you safe.
3. **[The 2am That Happens Twice](03-the-2am-that-happens-twice.md)** - daylight saving time creates gaps where time skips and overlaps where it repeats. The off-by-one-hour bug, the ambiguous timestamp, and why these are the rocks every naive time library splits on.


---

# A Moment Is Not a Clock Reading

Here's the reality you start from: you think of "time" as the number on a clock, 3:00pm. You and a friend agree to meet at 3pm and you both know what that means. So when you write code, you reach for the same thing - you store "3:00pm" and assume it's a fixed point. That assumption is the source of nearly every time bug you will ever write.

Because "3:00pm" is not a point in time. It's a clock reading. And clocks all over the world read different things at the same actual instant.

## The two things people call "time"

There are really two different concepts hiding under the word *time*, and confusing them is the original sin.

**An instant** is a single point on the universe's timeline. The moment a payment cleared. The moment a sensor fired. It happens once, everywhere, simultaneously. When that payment cleared, it cleared *at the same instant* for someone in Tokyo and someone in New York - even though one of them was eating breakfast and the other was asleep.

**A wall-clock reading** is what a clock on a particular wall, in a particular place, says at that instant. At the one instant the payment cleared, the clock in Tokyo said 11:00pm and the clock in New York said 9:00am.

```text
ONE instant on the timeline:
   |
   v
Tokyo wall clock:     11:00 PM   (Monday)
London wall clock:     2:00 PM   (Monday)
New York wall clock:   9:00 AM   (Monday)
Los Angeles clock:     6:00 AM   (Monday)
```

*What just happened:* a single instant produced four different clock readings - and even a different *day* in some places. The instant didn't change. The wall it's read off did. "3:00pm" with no location attached is not enough information to know *when* you mean.

## So what is an instant, in a computer?

If wall-clock strings are ambiguous, you need a way to name an instant that means the same thing everywhere. There are two common ones, and they're really the same idea.

**UTC** (Coordinated Universal Time) is a single global reference clock - think of it as the world's neutral wall clock, sitting at the prime meridian, that never shifts for daylight saving. When you say "this happened at 14:00 UTC," every machine on Earth agrees on exactly which instant you mean. UTC is the anchor everyone converts *to* and *from*.

**A Unix timestamp** is the same instant expressed as a plain number: the count of seconds (or milliseconds) since one fixed reference instant, midnight UTC on 1 January 1970, called the *epoch*. No time zone, no formatting, no ambiguity - only a number that ticks up by one every second, identically, on every computer in the world.

```text
Instant:           2026-06-30  14:00:00 UTC
Unix timestamp:    1782655200          (seconds since 1970-01-01 00:00 UTC)
```

*What just happened:* the same instant, written two ways. The UTC string is human-readable; the Unix number is what machines love - comparing two instants becomes comparing two integers, and arithmetic ("five minutes later") becomes adding `300`. Neither carries any "where," because an instant doesn't need one.

> A Unix timestamp is *not* "UTC time." It's a count of seconds with no zone at all. You convert it *into* a UTC string, or into a local clock reading, for display. The number itself is zone-free - that's the whole point of it.

## Why "3pm" needs a "where" to become a moment

Put the two ideas together and the rule falls out. A wall-clock reading by itself ("2026-06-30 15:00:00") is not an instant. It's an instant *only once you attach the place* - because the place tells you how far that wall clock sits from UTC.

```text
"2026-06-30 15:00:00" in Berlin   -> instant: 2026-06-30 13:00:00 UTC
"2026-06-30 15:00:00" in New York -> instant: 2026-06-30 19:00:00 UTC
```

*What just happened:* the exact same string of digits, "15:00:00," named two instants six hours apart, because Berlin's clock and New York's clock sit at different distances from UTC. The string alone is a riddle. The string plus the place is an answer.

This is the mental model to carry into everything else: **inside your program, work with instants** (UTC, or a timestamp) - unambiguous points on the timeline. **At the edges - when a human types a time or reads one - convert** between that instant and a local clock reading. The bugs happen when a wall-clock reading sneaks into the middle of your system pretending to be a moment.

## For builders

When a value crosses a boundary - comes out of a database, arrives in an API request, gets logged - ask one question: *is this an instant, or a clock reading?* An instant is safe to compare, sort, and store. A clock reading is display-only until you pair it with a zone. Training yourself to ask that one question, every time a date crosses a wire, prevents more time bugs than any library will. (If you're fuzzy on what "crossing a boundary" even means at runtime, [What Happens When Your Code Runs](/guides/what-happens-when-code-runs) lays out where data lives as a program executes.)

```quiz
[
  {
    "q": "At one single instant, the clock in Tokyo reads 11:00 PM Monday and the clock in London reads 2:00 PM Monday. What does this tell you?",
    "choices": [
      "One of the clocks is broken or set wrong",
      "A single instant produces different wall-clock readings depending on location",
      "Tokyo is in a different week than London",
      "Time moves faster in Tokyo than in London"
    ],
    "answer": 1,
    "explain": "One instant on the timeline shows up as different clock readings in different places. The instant is shared; the wall-clock reading is local."
  },
  {
    "q": "What is a Unix timestamp?",
    "choices": [
      "A clock reading in the UTC time zone, formatted as text",
      "A count of seconds since a fixed reference instant (1970-01-01 00:00 UTC), with no time zone",
      "The current local time on the server, as a string",
      "A date stored in the format the user's region prefers"
    ],
    "answer": 1,
    "explain": "It's a plain zone-free number counting seconds from the epoch. You convert it to UTC or local for display; the number itself carries no zone."
  },
  {
    "q": "Why is the string \"2026-06-30 15:00:00\" not, by itself, a moment in time?",
    "choices": [
      "It is a moment; the string already contains everything needed",
      "It is missing the year's day-of-week",
      "Without a place attached, you don't know how far that wall clock sits from UTC, so it could name many different instants",
      "It needs to be converted to a Unix timestamp first to be valid"
    ],
    "answer": 2,
    "explain": "A wall-clock reading becomes an instant only when you attach the location, which tells you its distance from UTC. The same string in Berlin and New York names instants hours apart."
  }
]
```


---

# Offsets, Zones, and the Golden Rules

You now know an instant is not a clock reading. The next trap is subtler, and it catches people who *think* they've got time figured out: treating an **offset** as if it were a **time zone**. They look similar. They are not the same thing, and the difference is exactly the thing that breaks twice a year.

## An offset is a number. A zone is a rulebook.

An **offset** is how far a local clock currently sits from UTC, written like `+02:00` or `-05:00`. It's a single number describing one moment. "Right now, Berlin is two hours ahead of UTC" - that's an offset.

A **time zone** is a named region with a *complete set of rules* about what its offset is, and - crucially - *when that offset changes*. `Europe/Berlin` is a zone. Its rules say: most of the year the offset is `+01:00`, but from late March to late October it's `+02:00`, and here are the exact instants it switches.

```text
Offset:    +02:00          a number. true for ONE moment. tells you nothing about tomorrow.

Zone:      Europe/Berlin   a rulebook. knows it's +01:00 in winter, +02:00 in summer,
                           and the precise dates it flips between them - past and future.
```

*What just happened:* the offset `+02:00` is a snapshot; `Europe/Berlin` is the whole film. If you store only `+02:00`, you've thrown away the rules - so you can't correctly compute what a Berlin clock will read three months from now, because you don't know whether daylight saving will have flipped by then. The offset can't tell you. The zone can.

This is why "store the offset" is a trap. The offset is right *today* and wrong after the next daylight-saving switch. The zone stays right forever, because it carries the rules.

## The IANA database: where the rules actually live

You might wonder: who decides that Berlin flips on the last Sunday of March? Governments do - and they change their minds, sometimes with only weeks of notice. Countries add daylight saving, drop it, shift their offset, redraw zone boundaries - messy, political, and constantly moving.

So nobody sane hand-codes these rules. There is one authoritative, community-maintained dataset that every serious system uses: the **IANA Time Zone Database** (also called *tz* or *zoneinfo*). It's the source of names like `America/New_York`, `Asia/Kolkata`, and `Europe/Berlin`, and for each one it records the full history *and* the current rules of that region's offset changes. When a government announces a change, the database is updated, and your operating system, language runtime, and libraries pull in the new version.

```text
IANA zone names look like   Area/Location
   America/New_York
   Europe/London
   Asia/Tokyo
   Australia/Sydney

Each entry encodes:  current offset(s)  +  the rules & dates for every change,
                     historical and future, as governments have defined them.
```

*What just happened:* the database turns "what time is it in Sydney" from a guess into a lookup. You name the zone; the library reads IANA's rules; you get the right offset for the specific instant you asked about - including the awkward instants around a daylight-saving switch. You never write these rules yourself, because they're not yours to know.

> Use IANA names (`Europe/Berlin`), never abbreviations like `EST`, `CST`, or `IST`. Abbreviations are ambiguous - `IST` means India, Ireland, *and* Israel time depending on who's talking - and they don't carry daylight-saving rules. The IANA name is the only label that's both unique and complete.

## The golden rules

Everything above collapses into four habits. Follow them and the whole hairy domain becomes boring, which is exactly what you want from time handling.

**1. Store and compute in UTC (or Unix timestamps).** Inside your system - your database, your business logic, your comparisons - work only with instants. They're unambiguous, they sort correctly, and arithmetic on them is reliable. Never store a local wall-clock time as your source of truth.

**2. Convert only at the edges.** A human types a local time on the way in → convert it to UTC immediately. You show a time on the way out → convert UTC to the viewer's local zone at the last possible moment. The conversions live at the boundary; the core never sees a local time.

```text
   user input "3pm Berlin"            display "shows as 9am New York"
            |                                       ^
            v  convert in                           |  convert out
   +---------------------------------------------------------+
   |   CORE: everything is UTC / Unix timestamps             |
   |   store, compare, sort, do math - all on instants       |
   +---------------------------------------------------------+
```

*What just happened:* this is the shape of every well-behaved time system. Local times exist only at the rim, as input and output. The middle is pure instants, so none of the daylight-saving chaos from Phase 3 can leak into your logic.

**3. Never hand-roll zone math.** Do not add or subtract hours yourself to "convert time zones." The moment you write `hour + 2`, you've assumed an offset that's wrong half the year, and you've ignored every IANA rule. This is the single most common way time code breaks.

**4. Use a real, IANA-backed library.** Every mainstream language has one - and the modern, correct one is usually *not* the old built-in date type your language shipped with originally. Reach for the library that knows zones: `ZonedDateTime` in Java, the `zoneinfo` module in Python, `Temporal` (or a library like Luxon/date-fns-tz) in JavaScript, `chrono-tz` in Rust, `time.LoadLocation` in Go. Let it do the lookups. Your job is to name the zone correctly and stay out of the way.

```python runnable
from datetime import datetime
from zoneinfo import ZoneInfo

# A human picks a wall-clock time in Berlin - convert to an instant at the edge.
berlin_local = datetime(2026, 6, 30, 15, 0, tzinfo=ZoneInfo("Europe/Berlin"))

# Store / compute as the unambiguous instant (UTC):
utc_instant = berlin_local.astimezone(ZoneInfo("UTC"))

# Display the same instant to a viewer in New York - convert at the other edge:
ny_local = utc_instant.astimezone(ZoneInfo("America/New_York"))

print("Berlin says: ", berlin_local.strftime("%Y-%m-%d %H:%M %Z"))
print("Same instant in UTC:", utc_instant.strftime("%Y-%m-%d %H:%M %Z"))
print("New York sees:", ny_local.strftime("%Y-%m-%d %H:%M %Z"))
```

*What just happened:* one instant, named once in Berlin's zone, then converted in and out. The library read IANA's rules to figure the correct offsets - you never wrote a single `+ hours`. That's all four rules in eight lines: convert at the edge, keep an instant in the middle, lean on the zone database, hand-roll nothing.

## For builders

Audit your storage. If a date column anywhere holds a local time with no zone, that's a latent bug waiting for a user in another region or the next daylight-saving switch. The fix is mechanical: store the instant (UTC / timestamp), and store the user's IANA zone *separately* if you need to reconstruct their local view. Two clean fields beat one ambiguous one every time.

```quiz
[
  {
    "q": "What's the difference between the offset +02:00 and the zone Europe/Berlin?",
    "choices": [
      "Nothing - they're two ways of writing the same thing",
      "The offset is a single number true for one moment; the zone is a rulebook that also knows when the offset changes",
      "The offset is more precise than the zone",
      "Europe/Berlin is an abbreviation for +02:00"
    ],
    "answer": 1,
    "explain": "An offset is a snapshot of distance-from-UTC. A zone carries the full rules, including the dates the offset flips - so only the zone survives a daylight-saving switch."
  },
  {
    "q": "What is the IANA Time Zone Database used for?",
    "choices": [
      "Storing every user's preferred date format",
      "Converting Unix timestamps to integers",
      "Recording the authoritative rules and history of each region's offset changes, under names like America/New_York",
      "Synchronizing computer clocks over the network"
    ],
    "answer": 2,
    "explain": "IANA (tz/zoneinfo) is the shared, maintained source of truth for zone rules. Libraries read it so nobody hand-codes when Berlin flips for daylight saving."
  },
  {
    "q": "Which practice follows the golden rules?",
    "choices": [
      "Store local wall-clock times and add or subtract hours when you need another zone",
      "Store and compute in UTC, convert to local only at input and output, and let an IANA-backed library do the zone math",
      "Store the current offset (like +05:00) alongside each timestamp as the source of truth",
      "Use abbreviations like EST and IST so the zone is human-readable"
    ],
    "answer": 1,
    "explain": "UTC in the core, conversions at the edges, no hand-rolled hour math, and a real zone-aware library - that's the whole safe recipe."
  }
]
```


---

# The 2am That Happens Twice

This is the phase where you learn *why* the rules from Phase 2 aren't bureaucratic fussiness - they're armor against one specific, recurring disaster. Twice a year, in any region that observes daylight saving time, the local clock does something genuinely strange: it skips an hour, then six months later repeats one. Every naive assumption about time - "each hour happens once," "I can add an hour by adding 3600 seconds to the clock reading" - breaks on exactly these two instants.

## Spring forward: the hour that never existed

In spring, clocks jump ahead. At the switch instant, the local clock goes straight from 1:59am to 3:00am. The hour from 2:00am to 2:59am *does not happen* on that calendar day. It's a **gap**.

```text
Local clock on the spring-forward day:

   1:58  1:59  ──jump──  3:00  3:01
                  ^
         2:00–2:59 never occurs locally
```

*What just happened:* if a user (or your code) constructs the local time "2:30am" on that day, you've named a wall-clock reading that *did not exist*. Ask a naive library to convert it and you get garbage, an error, or a silent guess. This is the **nonexistent time** problem - and you hit it any time you build a local time programmatically near a spring switch (think: a daily 2:30am cron job, or a "remind me at 2:30" that lands on the wrong day).

## Fall back: the hour that happens twice

In autumn, clocks fall back. At the switch instant, the local clock goes from 1:59am back to 1:00am and replays the whole hour. So "1:30am" happens *twice* - once before the fall-back, once after - and the two are different instants, an hour apart.

```text
Local clock on the fall-back day:

   1:00  1:30  1:59  ──fall back──  1:00  1:30  1:59  2:00
   \________first 1:30________/      \_______second 1:30______/
         offset +02:00                       offset +01:00
```

*What just happened:* "1:30am" is now **ambiguous** - it names two different instants. If you stored a local time "01:30" with no offset, you literally cannot tell which one you meant. Was the transaction at the first 1:30 or the second? An hour of difference, and the data can't say. This is why storing local wall-clock time as your source of truth (Phase 2, rule 1) loses information you can never recover.

## The off-by-one-hour bug, dissected

Now the classic. Someone needs "one hour after a local time" and writes the tempting thing:

```text
WRONG:  take the wall-clock reading, add 1 to the hour field
        1:30am  ->  2:30am     "see? one hour later."
```

On 363 days a year, that's correct and the bug hides. But do it across a spring-forward gap and "2:30am" never existed; do it across fall-back and the real elapsed time was *two* hours, not one, because an hour got replayed. The wall clock and the actual passage of time disagreed, and the naive code trusted the wall clock.

The correct version never touches the clock fields. It works on the instant:

```text
RIGHT:  convert local -> instant (UTC),
        add 3600 seconds to the INSTANT,
        convert back -> local for display
```

*What just happened:* by doing the arithmetic on the instant and letting the zone's rules handle the conversion back, "one hour later" means *one real hour of elapsed time*, every day of the year - including the two weird ones. The gap and the overlap are handled by the IANA rules inside your library, not by you. This is rule 3 ("never hand-roll") and rule 4 ("use a real library") earning their keep.

```python runnable
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

berlin = ZoneInfo("Europe/Berlin")

# An instant just before Berlin's autumn fall-back (clocks go +02:00 -> +01:00).
before = datetime(2026, 10, 25, 0, 30, tzinfo=ZoneInfo("UTC")).astimezone(berlin)

# Add one REAL hour by adding to the instant, then view it locally:
after = (before.astimezone(ZoneInfo("UTC")) + timedelta(hours=1)).astimezone(berlin)

print("Local before:", before.strftime("%H:%M %Z (offset %z)"))
print("Local after: ", after.strftime("%H:%M %Z (offset %z)"))
print("Wall clock read the same hour, but the offset (and the instant) moved.")
```

*What just happened:* across the fall-back, one real hour of elapsed time can leave the *displayed* hour looking unchanged while the offset shifts from `+0200` to `+0100`. If you'd "added one to the hour field" instead, you'd have skipped a real hour entirely. The library got it right because it reasoned about the instant, not the clock face.

## Why this is the deeper payoff

Daylight saving is the stress test that proves the whole mental model. The split from Phase 1 (instant vs. clock reading) and the rules from Phase 2 (UTC core, edge conversions, no hand-rolling, real library) aren't four unrelated tips - they're a single design that makes the gap and the overlap *somebody else's problem*. Get them right and the haunted 2am is another instant flowing through a system that only ever reasoned about instants. Get them wrong and you'll meet this bug once a year, always in production, always confusing, until you do.

## For builders

The two times a year the switch happens are the only times these bugs are observable - which is exactly why they survive code review and slip into production. Don't wait for the calendar to find them. Write a test that constructs an instant inside a known gap and a known overlap for a real zone (`Europe/Berlin` and `America/New_York` both work) and asserts your "add an hour" and "what's yesterday" logic does the right thing. A handful of such tests will catch the entire family before a user ever does.

```quiz
[
  {
    "q": "On the spring-forward day, the local clock jumps from 1:59am to 3:00am. What happens if your code constructs the local time \"2:30am\" that day?",
    "choices": [
      "It's fine - 2:30am is a normal time",
      "It names a nonexistent local time (a gap), so a naive conversion gives an error or a silent wrong guess",
      "It automatically becomes 3:30am with no issues",
      "It only matters if the user is awake at 2:30am"
    ],
    "answer": 1,
    "explain": "Spring-forward creates a gap: 2:00–2:59 never occurs locally that day. Building a time inside the gap is the nonexistent-time bug."
  },
  {
    "q": "On the fall-back day, \"1:30am\" occurs twice. Why is storing a local time \"01:30\" with no offset a problem?",
    "choices": [
      "It takes up more storage than UTC",
      "The two 1:30s are the same instant, so it doesn't matter",
      "It's ambiguous - \"01:30\" names two different instants an hour apart, and you can no longer tell which one you meant",
      "Local times can never be stored at all"
    ],
    "answer": 2,
    "explain": "Fall-back replays an hour, so the wall-clock reading maps to two instants. Without an offset or UTC, that information is lost permanently."
  },
  {
    "q": "What's the correct way to compute \"one hour after\" a local time so it works every day of the year?",
    "choices": [
      "Add 1 to the hour field of the wall-clock reading",
      "Convert to an instant (UTC), add 3600 seconds to the instant, then convert back to local with a zone-aware library",
      "Add 3600 seconds to the wall-clock string directly",
      "Avoid daylight-saving zones entirely"
    ],
    "answer": 1,
    "explain": "Doing the arithmetic on the instant - not the clock fields - means \"one hour\" is one real elapsed hour even across a gap or overlap. The library's IANA rules handle the conversion back."
  }
]
```
