DEV Community

Cover image for Save the moment in UTC, save the place by name: a beginner's guide to dates and timezones
Payyavula Balanagaraju
Payyavula Balanagaraju

Posted on Originally published at depthfirst.hashnode.dev

Save the moment in UTC, save the place by name: a beginner's guide to dates and timezones

If your app shows a date or a time to people in more than one place, this guide is for you. You don't need to
know anything about timezones to read it. By the end you'll know the three rules most teams follow, why they
work, and the one case where they are not enough.

I learned all of this the hard way, so let me start there.

How I learned this

When we started, we saved times the simple way: the local time someone typed, with no timezone attached.
For example 2026-12-05 10:00.

It looked fine. Then it broke quietly:

  • Courses closed an hour early, or an hour late.
  • Early-bird prices ended at the wrong moment.
  • A page showed a different time from the one that was saved.

The cause was the same every time. 2026-12-05 10:00 is not a moment. It's 10:00 somewhere. Without the
"where", every server, browser and job read it in its own timezone, and they didn't agree.

So I read the official sources, asked a lot of questions, and we changed how we store time. Here is what I
wish someone had told me on day one.

Step 1: a moment and a wall clock are two different things

  • A moment is one point in time, the same everywhere on Earth. "When the rocket launched."
  • A wall-clock time is what a clock on the wall shows in one place. "10:00 in Vancouver."

The same moment shows different wall-clock times in different places. 10:00 in Vancouver is 22:30 in
Bengaluru on the same moment.

Bugs happen when you save a wall-clock time and treat it like a moment.

Step 2: UTC is the reference clock

UTC (Coordinated Universal Time) is the world's reference clock. It never changes for daylight saving.
Every place on Earth is described as "UTC plus or minus some hours" at a given moment:

Place Offset today (Nov 2026)
Bengaluru UTC+5:30
Dublin UTC+0 in winter, UTC+1 in summer
Vancouver UTC−7, all year (new in 2026, see below)

If you save every moment in UTC, every server, browser and background job reads the same value. That fixes the
"which timezone did this mean?" problem at the root.

Step 3: save the place by name, never by offset

An offset like −07:00 is a number. A timezone name like America/Vancouver is a place with a
history of rules.

Offsets change. Dublin is +00:00 in January and +01:00 in July. Vancouver was −08:00 every winter until

  1. If you save −08:00, you've saved a fact that was true for one date only.

If you save America/Vancouver, the software works out the right offset for any date: past, present or
future, daylight saving included. You write no daylight-saving code at all.

Names look like Continent/City: America/Vancouver, America/Edmonton, Europe/Dublin, Asia/Kolkata.
(Note: it's Asia/Kolkata and Europe/Dublin, not "India/Calcutta" or "Ireland/Dublin". Use the official
list.)

Step 4: who keeps these rules? The IANA timezone database

Somebody has to record that "Vancouver stopped changing clocks in 2026". That somebody is the IANA Time Zone
Database
(also called tzdata or the Olson database), a small group of volunteers who publish the rules for
every place on Earth, going back decades.

When a government changes its clocks, the database ships a new release. Operating systems, Java, Postgres,
Node.js and libraries like moment-timezone copy that data into themselves.

That last part matters: the timezone database is a dependency, like any library. If you don't update it,
your app uses old rules.

And you probably have more than one copy:

  • the server's operating system,
  • your runtime (Node.js, Java),
  • your date library (moment-timezone bundles its own),
  • your database (Postgres ships its own copy unless it was built to use the system's),
  • every user's browser.

They can disagree for weeks after a change.

Step 5: what changed in Canada in 2026 (a real example)

This year four parts of Canada stopped changing their clocks. The database needed four releases to keep up:

Release Date Change
2026b 22 Apr 2026 British Columbia: UTC−7 all year
2026c 8 Jul 2026 Alberta: UTC−6 all year
2026d 11 Sep 2026 Northwest Territories: UTC−6 all year
2026e 29 Sep 2026 Manitoba: UTC−5 all year

British Columbia moved its clocks forward for the last time on 8 March 2026. On 1 November, when clocks would
normally go back, they stayed put.

Why Vancouver now shows "MST"

Short labels like PST and MST are not decoration. Each one names an offset:

Label Means
PST Pacific Standard Time, UTC−8
PDT Pacific Daylight Time, UTC−7 (daylight saving)
MST Mountain Standard Time, UTC−7

Vancouver is now at UTC−7 all year, with no daylight saving. PST would be an hour wrong. PDT would say daylight
saving is on, and it no longer exists there. So the database labels it MST, the long-standing name for
UTC−7 standard time in North America. For the same reason, Edmonton (now UTC−6 all year) is labelled CST.

Two honest notes:

  • The provinces use their own names: British Columbia calls it "Pacific Time" (PCT), Alberta calls it "Alberta Time". The database's 2026b notes say a future release may change the label.
  • Windows and browsers don't always show the database's label. Browsers use another project (Unicode CLDR) for display names, so you may see "GMT-7" or a long name instead.

Our choice was to show what the database says. If the database changes the label later, our screens change
with the next update, with no code. If you override labels by hand, you own that list forever. And never
search-and-replace "MST" with something else: Arizona really is MST all year, and so is Denver in winter.

Step 6: what this looks like in code

In the database (Postgres): use timestamptz for moments, and keep the zone name next to it.

CREATE TABLE course (
  id         bigint PRIMARY KEY,
  starts_at  timestamptz NOT NULL,  -- a moment; Postgres stores it as UTC
  time_zone  text        NOT NULL   -- IANA name, e.g. 'America/Vancouver'
);

-- Show it as the local wall-clock time of the course's place
SELECT starts_at AT TIME ZONE time_zone AS local_start
FROM course;
Enter fullscreen mode Exit fullscreen mode

Despite its name, timestamptz doesn't store a timezone. It stores the moment in UTC and converts it for
display using your session's TimeZone setting. The plain timestamp type (without time zone) is the
"local time with no zone" trap from the start of this guide.

In JavaScript: a Date is already a moment (milliseconds since 1970 in UTC). Convert only when you display it.

const startsAt = new Date('2026-12-05T17:00:00Z'); // the moment, from the API, in UTC

startsAt.toLocaleString('en-CA', {
  timeZone: 'America/Vancouver', // the place, by name
  dateStyle: 'medium',
  timeStyle: 'short',
});
// 10:00 a.m. on 5 Dec 2026 in Vancouver (with up-to-date timezone data)
Enter fullscreen mode Exit fullscreen mode

In background jobs: schedule in UTC. "Close this course at 2026-12-05T17:00:00Z" fires at the right moment
whether the course is in Vancouver, Dublin or Bengaluru. Our job queue only ever deals in UTC, and that
removed a whole class of bugs.

Step 7: the one case where UTC alone breaks: future events

Here's the trap that almost nobody mentions in beginner guides.

Say in February 2026 someone creates an event: 10:00 on 5 December 2026, in Vancouver. At that time the
rules said Vancouver would be at UTC−8 in December. So the app saved 2026-12-05T18:00:00Z.

Then British Columbia changed its law. Vancouver will be at UTC−7 in December instead. Now that saved moment,
18:00Z, shows as 11:00 in Vancouver. The event is an hour off, and nobody touched it.

The person meant "10:00 on the wall clock in Vancouver". We saved a moment computed with rules that later
changed.

What to do about it:

  • For things that already happened (payments, logins, check-ins): UTC is exactly right. The past doesn't change.
  • For future events people plan by the wall clock (a class at 10:00 local): also keep the local time and the zone name the person chose, and recompute the UTC moment from them when the rules change. Or at least check future events after every timezone-data update.

Jon Skeet wrote the classic piece on this: Storing UTC is not a silver bullet (link at the end). Read it
once you're comfortable with the basics.

A checklist to keep

  1. Save moments in UTC (timestamptz in Postgres, ISO strings ending in Z in APIs).
  2. Save the place as an IANA name (America/Vancouver), never as an offset.
  3. Convert to local time only at the last step, when a person reads it.
  4. Schedule jobs in UTC.
  5. Treat the timezone database as a dependency: update it, and know every copy you run.
  6. For future wall-clock events, keep the local time + zone name too, and re-check after rule changes.
  7. Show the label the database gives you. Don't hand-edit labels.

What I'd tell a new developer

UTC tells you when. The zone name tells you where. You need both, and the local time is something you work
out at the end, not something you store.

What's the one timezone rule you wish someone had told you earlier?


Sources

Top comments (0)