How to Handle Time Zones Without Losing Your Mind

Store everything in UTC is good advice that is also incomplete. It fails the moment someone schedules a meeting for next March.

How to Handle Time Zones Without Losing Your Mind — Troiana insight cover

In short

Store instants in UTC and convert at the edges. But for anything a person scheduled in the future — a recurring meeting, a reminder, a booking — also store the IANA time zone name, because governments change offsets and a UTC instant computed today can be the wrong wall-clock time later. Never store an offset like +02:00 as a substitute for a zone.

Two different kinds of time

Most time bugs come from treating these as the same thing.

An instant is a moment that happened, everywhere at once. A payment, a log entry, a message send. UTC is exactly right for these — store the instant, convert for display.

A wall-clock intention is a human commitment to a local time. "The standup is at 9am." That is not an instant; it is a rule that produces different instants on different days, and it can change without anyone touching your database.

The classic failure: computing the UTC instant for a recurring 9am meeting once, storing it, and watching every occurrence shift by an hour when the clocks change.

The rules that prevent most bugs

Store past events as UTC instants. Use a timestamp type that carries a time zone if your database offers one. Convert to local only for display.

Store future human commitments as local time plus an IANA zone nameEurope/Belgrade, not +02:00. The zone name is a reference to a set of rules; the offset is only what those rules produce today.

Never store an offset as a zone. +02:00 tells you nothing about what happens in November. Governments change daylight-saving rules with only months of notice, and when they do, every offset you stored is silently wrong.

Do arithmetic in the right frame. "Tomorrow at the same time" means the same wall-clock time in the user's zone, which may be 23 or 25 hours later. Adding 86,400 seconds to a UTC instant is a different operation, and around a clock change it gives the wrong answer.

Use a maintained library. Time zone rules are political and change several times a year. Hand-rolled offset arithmetic is always wrong eventually.

The gaps and overlaps nobody tests

Twice a year, local time does something that breaks naive code.

Spring forward creates a gap. In zones that skip 02:00 to 03:00, the time 02:30 does not exist on that date. A user scheduling something at 02:30, or a job that runs "daily at 02:30", has no valid instant. Libraries handle this differently — some throw, some shift forward. Decide which behaviour you want rather than inheriting one.

Autumn back creates an overlap. 01:30 occurs twice. Any lookup by local time is ambiguous, and a job scheduled then can fire twice — which is why idempotency matters for scheduled work as much as for API calls.

Test both explicitly. They are the two dates a year when scheduling systems fail, and they are entirely predictable.

Dates without times are their own trap

A birthday, an invoice date, a public holiday. These are calendar dates, not instants.

Store them as a date type. The moment you convert a date to a timestamp — midnight in some zone — you have introduced a bug, because midnight in one zone is the previous day in another. Users west of you will see their birthday on the wrong day.

If your language lacks a date-only type, store the ISO string and resist the temptation to parse it into a datetime.

Which zone is "the" zone

For anything involving more than one person, be explicit about whose clock governs.

The user's zone for personal reminders and displays.

The organisation's zone for business rules — "invoices generate on the first of the month" needs one authoritative zone, or a company spanning several will generate them repeatedly.

The event's zone for anything tied to a place. A conference at 09:00 in Prishtina is 09:00 there regardless of where an attendee sits.

Store the zone alongside the time whenever the answer is not obvious. A booking system that records a slot without recording which zone it was booked in cannot be reasoned about later.

Getting the user's zone right

Browsers report the zone reliably, and it is the sensible default. But let people change it, and store their preference rather than re-detecting each visit — travellers otherwise see their entire schedule shift when they land.

When displaying a time to someone in a different zone, say which zone you mean. "14:00 CET" removes an entire class of confusion, and costs four characters.

Displaying time honestly

Relative time — "3 hours ago" — is friendly and imprecise. Fine for recent activity; poor for anything auditable. Pair it with the absolute value on hover or alongside.

Absolute time needs its zone. A timestamp in a log or an export without a zone is a guess.

Format for the reader, not the developer. 2026-09-03T14:00:00Z belongs in an API. 3 September 2026, 14:00 CET belongs on a page.

A short checklist

Before shipping anything time-related: are past events stored as UTC instants; are future commitments stored with an IANA zone name rather than an offset; are calendar dates stored as dates; is there an authoritative zone for business rules; and have you tested the spring gap and the autumn overlap?

Most time bugs are one of those five, and all five are cheaper to get right at the start than to unpick from production data.

If you are building scheduling or billing where a one-hour error is expensive, book a call.

Common questions

Should I store everything in UTC?

Store past events as UTC instants, yes. But future human commitments — a recurring meeting, a scheduled reminder — need the local time plus an IANA zone name as well, because a UTC instant computed today can become the wrong wall-clock time when daylight-saving rules change.

Why shouldn't I store a UTC offset like +02:00?

Because an offset is only what a zone's rules produce today. It tells you nothing about what happens after a clock change, and governments alter daylight-saving rules with just months of notice. Store the IANA zone name such as Europe/Belgrade, which is a reference to the rules rather than to one result of them.

What breaks at daylight saving transitions?

Two things. Spring forward creates a gap where a local time such as 02:30 does not exist on that date, so scheduling at that time has no valid instant. Autumn back creates an overlap where 01:30 occurs twice, making lookups ambiguous and letting scheduled jobs fire twice.

How should I store a date of birth or an invoice date?

As a date type, not a timestamp. Converting a calendar date to midnight in some zone introduces a bug immediately, because midnight in one zone falls on the previous day in another — users west of your chosen zone will see the date shift by one day.

Which time zone should business rules use?

One authoritative zone, chosen explicitly. A rule like 'invoices generate on the first of the month' needs a single governing zone, or a company operating across several will generate them more than once. Store the zone alongside the time whenever the correct answer is not obvious from context.

Have something worth building right?