TimezoneSearch

Why You Should Store Every Timestamp in UTC (and Convert Only for Display)

6 min readtechnicaldevelopers

A job in São Paulo fires at 8:00 instead of 9:00, one morning, in a service nobody deployed. Brazil abolished daylight saving in 2019. America/Sao_Paulo in a current copy of the time zone database is UTC−3 all year, so 09:00 local is 12:00 UTC. An old copy still applies the pre-2019 rule, which put the country on UTC−2 for the southern summer. That copy converts 09:00 to 11:00 UTC. The job runs at 11:00 UTC, and the wall clock in São Paulo says 8:00. The code path is the same path. The rules file is the thing that is wrong, or the thing that was frozen at write time.

A local wall time is not an instant

“2:30 AM in America/New_York” depends on the daylight-saving rules for that date. Governments move those rules, sometimes with a few months’ notice, sometimes for a stretch of years and then not. If you store the wall time as a string and convert it later, you are betting that the rules at read time match the rules you assumed at write time.

2026-03-08T07:30:00Z is one instant. It stays that instant if New York, São Paulo, or Morocco changes a law next year. The civil time you show for it is a rendering, and the rendering is allowed to change when the database changes. The instant is not.

The vocabulary is in UTC versus GMT. UTC is the scale. GMT is a solar definition and, in winter, a UK zone name. Do not store the UK zone name and hope.

The hour that does not exist

On the second Sunday of March, US clocks jump from 1:59 AM to 3:00 AM. In 2026 that Sunday was 8 March. America/New_York had no 2:30 AM. A library asked to construct “2026-03-08 02:30” in that zone has to invent a policy: reject it, roll it forward, or roll it back. Libraries do not agree. A payment, a reminder, or a cron expression that landed in the gap will do whatever that library does, which is a poor thing to discover from two regions an hour apart in the ledger.

The rule, not the date, is what you test. The second Sunday of March is the gap every year under the current US law. 8 March 2026 is only that year’s example.

The hour that happens twice

On the first Sunday of November the same wall time occurs twice. In 2026 that Sunday was 1 November. 1:30 AM America/New_York happened at 05:30 UTC, while the zone was still UTC−4, and again at 06:30 UTC, after the fall back to UTC−5. A record that says “1:30” with no offset is two possible events, an hour apart. A log sorted on that string is not sorted in time.

A rule change with no exception

The São Paulo case is the quiet one, because nothing throws. The old rules and the new rules both produce a plausible instant. They disagree by an hour for the months when the abolished daylight period used to apply, roughly October through February. The same class of bug is sitting under any zone whose government has moved a date since your process last updated tzdata. Morocco’s return to permanent UTC+0 on 20 September 2026 is a fresh one. A runtime that still has the 2018 decree will be an hour off outside Ramadan, and right, by accident, during the weeks when the old rule also said UTC+0.

“About 200 countries” is not a measurement of how often this happens. I am not going to invent a rate. It happens often enough that a pinned offset table is a bug with a publication date.

A daily 8:00 is a rule, not an instant

A reminder set for 8:00 AM every day, stored as one UTC timestamp, stops being 8:00 AM the morning the offset changes. The instant did not move. The meaning did.

Store the rule: 08:00, America/Chicago, daily. Compute the UTC instant for each occurrence from the current rules when you send it. The same shape covers a billing run tied to a local business day, and a meeting that is supposed to stay at 9:00 AM in New York rather than at a frozen UTC hour. The calendar version of that choice is which time zone should own the invite.

The layering I want in a codebase

Store and transmit instants in UTC. An ISO 8601 string with a Z, or a Unix timestamp, does not depend on a legislature. When a person types “9:00 AM,” save the IANA zone with it and convert to UTC immediately. An offset like “−05:00” is not a zone. It is a fact about one date, and it will be the wrong fact after the next spring forward.

When you show a timestamp, convert at render time from a current tz database. When the event is “every weekday at 9:00 AM New York time,” store that sentence, not the UTC instant of next Wednesday.

A test suite that runs in UTC, on a Tuesday in June, will not catch any of this. Include one case inside the missing hour, one inside the repeated hour, and one date in a zone whose rules changed after the fixture was written. São Paulo in January, and Casablanca after September 2026, are cheap fixtures. They fail in the useful direction: the assertion is an hour off, and the stack trace points at the conversion, not at a customer.

This site’s converter does not store your timestamps. It asks the browser’s Intl implementation, which uses the tzdata the browser shipped. Two updated browsers agree. A browser that missed the Morocco update does not. That limit is the same limit as a server image you have not rebuilt.