Timestamps, time zones and the hour that happens twice

What Unix time actually counts, how ISO 8601 offsets differ from IANA time zones, what daylight saving does to alarms and durations, and where storing UTC is the wrong answer.

A timestamp answers one of two questions, and most date bugs come from mixing them up. It names either an instant, a point on the timeline every observer agrees on, or a wall clock reading, which means nothing until you know whose wall. A payment happened at an instant; a dentist appointment is a wall clock reading. Store one as the other and it breaks later, by exactly one hour, on a Sunday morning.

Unix time counts, it does not tell the time

Unix time is the number of seconds elapsed since 1970-01-01 00:00:00 UTC. That UTC says where the counting started; the value itself carries no time zone. The same instant is the same number in Amsterdam, Auckland and Lima.

The problem is the unit, so count the digits.

DigitsExampleUnit
101756080000seconds
131756080000000milliseconds

Every present-day date has 10 digits in seconds and 13 in milliseconds, and will until 2286. Both misreadings are loud: milliseconds read as seconds land in the year 57626, seconds read as milliseconds in January 1970. A timestamp converter showing the interpreted date makes it obvious.

The other limit is the container. A signed 32 bit count of seconds ends at 2147483647, which is 03:14:07 UTC on 19 January 2038, then wraps negative and reads as December 1901. Anything storing a 30 year expiry already computes past that. Unsigned 32 bit reaches 2106 but loses every date before 1970; 64 bit has no practical ceiling.

ISO 8601 and RFC 3339

The shape worth memorising is 2026-08-25T14:30:00Z: date, a T, time, then a zone designator. RFC 3339 is a stricter profile of ISO 8601 for internet protocols, and it requires that designator. ISO 8601 itself also allows week dates, a separator-free form such as 20260825T143000Z, and timestamps with no offset, so an API promising "ISO 8601" usually means the RFC 3339 subset.

Z means an offset of zero. +02:00 means the local clock is two hours ahead of UTC, so UTC is found by subtracting; reversing that is a common off by two error.

StringWhat it names
2026-08-25T14:30:00ZAn instant, unambiguously
2026-08-25T16:30:00+02:00The same instant, on a clock two hours ahead
2026-08-25T14:30:00A wall clock reading, no instant
2026-08-25A date, 23, 24 or 25 hours long

A string with neither Z nor an offset is ambiguous, and parsers disagree: some assume UTC, others the system zone. That is why a date-only value such as a birthday often renders a day early west of Greenwich.

An offset is not a time zone

+02:00 is a fact about one instant in one place. Europe/Amsterdam is a rule set: +01:00 in winter, +02:00 in summer, and a different shape in earlier decades. Storing the offset keeps the answer for the instant it was measured and discards the ability to compute any other.

Offsets are not unique either. On a summer afternoon +02:00 covers Amsterdam, Lagos and Johannesburg, so from the offset alone there is no route back to the rules and no way to know what that clock reads next December. Abbreviations are worse still, since CST names three different zones. Store the IANA identifier in Area/Location form.

The hour that does not exist and the hour that happens twice

Across the EU, clocks move forward on the last Sunday in March and back on the last Sunday in October. In Amsterdam, 02:00 becomes 03:00 in spring, so every local time from 02:00:00 to 02:59:59 does not occur that day. In autumn 03:00 becomes 02:00, so 02:30 happens twice, once at +02:00 and again an hour later at +01:00.

  • Recurring alarms. A job set for 02:30 local has no valid time on the spring Sunday. Schedulers differ: skip it, shift it to 03:00, or shift it to 01:30. On the autumn Sunday the same job may fire once or twice, so check the next few fire times with a cron parser.
  • Log files. An hour of local timestamps without an offset cannot be sorted, and two events an hour apart carry identical text. Log in UTC, or put the offset on every line.
  • Durations. From 20:00 on 24 October 2026 to 20:00 the next day, Amsterdam local, is 25 hours. "Add one day" and "add 24 hours" are different operations, and a date difference in calendar days answers a different question from a duration converter working in hours.

The rules change, so storage has to survive them

The IANA time zone database, tzdata, holds these rules and is published several times a year, because the rules are legislation: governments abolish daylight saving, adopt it, or move the switch dates, sometimes with weeks of notice. A pinned container image, a browser and a language runtime each carry their own copy and can disagree on one machine. Converting a future local time to an instant is a prediction, settled only when it arrives.

Kind of valueStoreReason
Something that happenedInstant in UTCThe past cannot be relegislated
Future appointment on a wall clockLocal date-time plus IANA zone idThe intent is the clock reading
Future instant, fixed across zonesUTCThe intent is the instant
Date with no timePlain date, no zone, no midnightAttaching a zone makes it drift

This is why "just store UTC" is wrong for a meeting next year. Convert 09:00 on 30 March 2027 in Europe/Amsterdam to UTC today and the answer is 07:00Z. If the switch date moves before then, the meeting is still at 09:00 local but 07:00Z now renders as 08:00: the stored value froze a prediction and discarded the intent. Keep local time plus zone as the source of truth; any UTC column beside it is a cache.

Showing dates to people

03/04 cannot be disambiguated from the string. It is 3 April to most of the world and 4 March in the United States; 03/04/05 adds a third unknown. Spell the month as a word for human output (4 Apr 2026) and use ISO for machine exchange. A Discord timestamp tag sidesteps this in chat by rendering in the zone and locale of whoever reads it: store an unambiguous value, format at the edge.

Week numbers have their own rule. ISO weeks start on Monday, and week 1 is the week containing the first Thursday of January, equivalently the week containing 4 January. So 1 January can fall in week 52 or 53 of the previous ISO year, and 29 December can fall in week 1 of the next. A common North American convention numbers instead from the Sunday-starting week containing 1 January, so the same date gets two numbers, and a weekday calculator's ISO week is worth checking against a spreadsheet.

Durations, months and leap years

"One month later" is not a fixed number of days; it is 28, 29, 30 or 31. Adding a month to 31 January has no correct answer, and libraries generally clamp to the end of February, which makes the operation non-reversible: add a month, subtract it again, and 31 January is now 28 January. Rounding is the same trap in miniature: truncating a clock time to the nearest quarter hour and rounding to it differ by up to a full interval.

The leap year rule in full: a year is a leap year if it is divisible by 4, except that years divisible by 100 are not, unless they are also divisible by 400. So 1900 was not, 2000 was, and 2100 will not be, giving an average year of 365.2425 days. Code that tests only divisibility by 4 is correct for every year in living memory, which is why it survives until 2100. A 29 February anniversary is the related nuisance: the next year offers 28 February and 1 March, and no rule chooses between them, so pick one deliberately.