Time Zones vs UTC Offsets - Why +09:00 Is Not Enough

If you have ever chased a bug where the timestamp is off by one hour despite carrying +09:00, or where the date differs between production and your laptop, the cause is usually the same: treating a time zone and a UTC offset as the same thing. They are different concepts, and they belong in different columns.

Key point: +09:00 is the offset from UTC at one instant. Asia/Tokyo is the full set of rules a region has used, including when those rules changed. You cannot recover the second from the first.

An offset only describes one instant

2026-08-03T10:00:00+09:00 says that this particular timestamp is nine hours ahead of UTC. It does not say the region is always nine hours ahead.

Regions with daylight saving time make this obvious. New York is -05:00 in winter and -04:00 in summer. Storing -05:00 alone does not tell you the value came from New York: other regions share that offset, and New York itself changes in summer.

Japan has a case too. Japan observed daylight saving time from 1948 to 1951, when summer time was +10:00. A system that hardcodes +09:00 converts dates in that window incorrectly.

What an IANA time zone name carries

Asia/Tokyo and America/New_York are identifiers from the IANA time zone database. They are not just labels: each one carries the history of when the offset changed and to what.

UTC offset (+09:00)IANA name (Asia/Tokyo)
DescribesThe offset at one instantA region's rules over time
Daylight savingCannot express itIncludes the transition points
Historical datesCannot recover past rulesConverts using the rules then in force
Future rule changesCannot follow themFollows via database updates
Use forRecording the instant something happenedRecording which region a time belongs to

Deciding what to store

Something that already happened

Store UTC. Login times, order timestamps, and updated-at columns all fall here. The instant is fixed, and later rule changes do not move it. Convert to the viewer time zone when rendering.

-- PostgreSQL
created_at timestamptz NOT NULL DEFAULT now()

A future appointment

Do not convert it to UTC up front. A meeting at 9:00 local time on 2027-10-01, stored as UTC, will drift if the region changes its rules before then. Countries do abolish daylight saving time and move transition dates.

Store the local date-time and the IANA name separately.

starts_at_local  timestamp    -- 2027-10-01 09:00:00 (no zone)
time_zone        text         -- 'Asia/Tokyo'

Compute the actual instant at display or notification time, using the tz database as it stands then.

ISO 8601 notation

NotationMeaning
2026-08-03T01:00:00Z01:00 UTC. Z is equivalent to +00:00
2026-08-03T10:00:00+09:00The same instant, expressed in a +09:00 region
2026-08-03T10:00:00No zone stated. Meaning depends on the reader
2026-08-03Date only; carries no time information

When exchanging timestamps over an API, always include the offset. A bare local string is one of the most common sources of disagreement between sender and receiver.

Check how your runtime behaves

// JavaScript: format in an explicit time zone
new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'full',
  timeStyle: 'long',
}).format(new Date('2026-08-03T01:00:00Z'));

// Check the runtime's own time zone
Intl.DateTimeFormat().resolvedOptions().timeZone;  // e.g. "Asia/Tokyo"

Code that depends on the server's time zone setting behaves differently in production and on a laptop. Specifying the time zone explicitly, rather than relying on the environment, is the most reliable way to avoid this class of bug.

Note: The tz database is updated several times a year to reflect legal changes. A container image with stale tzdata will convert post-change dates incorrectly, so include it in your regular update process.

Frequently Asked Questions

Is Asia/Tokyo the same as +09:00?

Today both describe a time nine hours ahead of UTC, but they mean different things. +09:00 is a fixed offset for one instant; Asia/Tokyo is the set of rules a region has used over time. Japan observed daylight saving time from 1948 to 1951, during which Asia/Tokyo was +10:00. For historical or future dates, the region name carries more information.

What should I store in the database?

For something that already happened, store UTC and convert to the viewer time zone at display time. Future appointments are different: converting them to UTC in advance means the appointment shifts if the region changes its rules. Store the local date-time and the IANA time zone name separately for future events.

What does the Z in ISO 8601 mean?

It denotes UTC and is equivalent to +00:00. 2026-08-03T01:00:00Z is the same instant as 2026-08-03T10:00:00+09:00. A value with no suffix, such as 2026-08-03T10:00:00, does not state which time zone it belongs to, so its meaning depends on whoever reads it.

← Back to the tech blog index