ToolsAre.Us — Next-Gen Tools Hub ToolsAre.Us — Next-Gen Tools Hub

Why time zones are harder than they look

Hours that happen twice, hours that never happen, and why you should never store an offset.

Last updated 2 September 2026 · ToolsAre.Us Guides

Time zones look like simple arithmetic. Add or subtract some hours and you are done. Almost every developer believes this once, and almost every developer is eventually corrected by a bug that only appears twice a year. The difficulty is not that the arithmetic is hard — it is that time zones are political rather than mathematical, and politics does not do consistency.

The vocabulary, precisely

Three terms get used interchangeably and should not be.

UTC (Coordinated Universal Time) is the reference. It has no daylight saving, never shifts, and is the same everywhere. It is what every other time is defined relative to.

An offset is a fixed difference from UTC, written like +05:30 or -08:00. It describes a moment, not a place. -08:00 tells you the arithmetic to apply right now; it does not tell you where you are or what will happen next month.

A time zone is a geographic region together with its complete history and future of offsets, including every daylight-saving transition and every political change. America/Los_Angeles is a time zone. -08:00 is not — it is one of the two offsets that zone uses during the year.

That distinction is the single most important idea here, and most time-zone bugs are a case of storing an offset where a zone was needed.

Offsets are not fixed, and not all whole hours

Daylight saving means most zones use two different offsets during a year. So "California is UTC-8" is true in January and false in July, when it is UTC-7.

Offsets are also not all whole hours. India runs at UTC+05:30 for the entire country. Iran uses +03:30, Nepal +05:45, and parts of Australia +08:45. Any code assuming offsets are whole numbers of hours is already wrong for well over a billion people.

Nor are zones tidily bounded by longitude. China spans five geographic hours and uses one zone for all of it. Spain sits west of Greenwich but keeps central European time for historical reasons. Some zones shift by 30 minutes at a daylight-saving transition rather than 60.

The two impossible hours

Daylight saving transitions create two situations that break naive code, and both are worth understanding concretely.

The hour that never happens

When clocks spring forward, local time jumps from 01:59:59 straight to 03:00:00. The entire 02:00–02:59 hour does not exist that day. A reminder set for 02:30 refers to nothing. If your code constructs that local time and asks for the corresponding instant, behaviour varies wildly between libraries: some throw, some shift forward an hour, some shift back, some silently return something arbitrary.

The hour that happens twice

When clocks fall back, local time reaches 01:59:59 and returns to 01:00:00. Every local time in that hour is ambiguous — 01:30 occurs twice, roughly an hour apart, and a local timestamp alone cannot say which. This is why logs written in local time are genuinely unusable for one hour every year: entries appear out of order, and durations computed across the boundary come out an hour wrong or negative.

Both problems disappear entirely if timestamps are recorded in UTC and converted only for display.

The IANA database, and why it keeps changing

The rules live in the IANA time zone database (also called tz or zoneinfo), the shared source of truth used by essentially every operating system and programming language. It contains zone identifiers in Area/Location form — Europe/London, America/New_York, Australia/Sydney — along with their full transition history, in many cases back to the nineteenth century.

It is updated several times a year, because governments keep changing the rules, frequently at short notice:

Sometimes the notice is a matter of weeks. This is why a system that has not updated its tz data can be an hour wrong even though its clock is perfectly synchronised — the clock is right and the rules are stale.

Why zone names are historical, not descriptive

Identifiers use a major city rather than a country because political boundaries change more often than the need to name a rule set. There is no Europe/France; there is Europe/Paris. When zones diverge, a new identifier can be added without invalidating the old one. The database also keeps deprecated aliases working, so old data does not break.

Abbreviations, by contrast, are a genuine trap. CST means US Central Standard Time, China Standard Time or Cuba Standard Time depending on context. IST is Indian, Irish, or Israeli. Abbreviations are ambiguous and should never be stored as the authoritative zone.

The rules that avoid nearly every bug

  1. Store instants in UTC. Anything that records when something happened — a log line, a created-at column, a measurement — is a point in time. Store it in UTC and convert only when displaying.
  2. For future events, store the local time and the zone identifier, not a computed UTC instant. This is the rule people get wrong most often, and the reasoning is below.
  3. Never store an offset alone. +02:00 loses the information needed to work out what the offset will be on a later date.
  4. Never do arithmetic on local times. Convert to an absolute instant, do the arithmetic, convert back.
  5. Keep tz data current. Treat it as data that expires, like a certificate.
  6. Use the platform's zone library. Every mainstream language has one. Hand-rolled offset tables are wrong within a year.

Why future events are different

Suppose someone schedules a meeting for 9 a.m. on 15 March next year in Europe/London. If you convert immediately and store 09:00 UTC, you have baked in today's belief about next March's rules. If Parliament then moves the transition date — or abolishes daylight saving, which has been debated repeatedly across Europe — your stored instant is now an hour off, and the meeting fires at the wrong time.

Storing "09:00, Europe/London" keeps the user's actual intention, which was nine in the morning local time, not a particular absolute instant. Resolve to UTC when you need it, using the rules current at that moment.

Past events are the opposite: the instant is settled and cannot change, so UTC is exactly right.

Scheduling across zones

A recurring call at 09:00 New York and 14:00 London holds for most of the year, and breaks for a few weeks in spring and autumn, because the US and the EU change clocks on different dates. For roughly three weeks in March and one in autumn, the gap is four hours rather than five. Southern-hemisphere participants are further complicated, since their daylight saving runs in the opposite half of the year, so a Sydney–London gap swings across a two-hour range.

Practical advice: anchor a recurring meeting to one zone and let the others fall where they may, rather than fixing local times in several zones and pretending they will stay aligned. And when confirming a one-off time across zones, state the date and the zone explicitly — "14:00 UTC on 15 March" is unambiguous in a way that "2pm your time" never is.

A short list of things that are simply not true

Every one of these assumptions is embedded in production code somewhere, waiting for the right date.

← All guides