Skip to content
Innovialabs

Dates and Time Zones in Code: UTC, ISO 8601 and Common Bugs

Practice4 min

In short

Store instants in UTC, exchange them as ISO 8601 strings with an offset, and convert to local time only when displaying to a user, using an IANA zone such as Asia/Tashkent.

The short answer

Most date bugs come from mixing up two different things:

  • an instant — a point on the global timeline, the same everywhere: "the order was created";
  • a calendar date or local time — what a person sees on a clock in their city: "birthday on March 5", "meeting at 10:00 Tashkent time".

The rule is simple: store instants in UTC, exchange them as ISO 8601 with an explicit offset, and convert to local time only at the last step, when showing it to a user. Store calendar dates without a time as dates, not as midnight UTC.

ISO 8601: one format for exchange

ISO 8601 is the date notation that every language and database understands:

ValueExample
Date2026-03-05
Instant in UTC2026-03-05T07:30:00Z
Instant with offset2026-03-05T12:30:00+05:00

The Z means UTC. A string with neither Z nor an offset (2026-03-05T12:30:00) is local time in an unknown zone. It is best not to accept such strings in an API at all.

An offset is not a time zone

+05:00 is an offset, while Asia/Tashkent is an IANA zone: it carries the history of rules, including daylight saving time (DST) and legal changes. Tashkent currently has no DST, but your users in Europe or the US do. If you stored only the offset, you cannot correctly compute "the same hour next week" after the clocks change.

So for recurring events and schedules, store local time plus the zone name; for facts that already happened, store the instant in UTC.

Common bugs

  • Off-by-one day. A birthday saved as 2026-03-05T00:00:00Z shows up as March 4 for a user with a negative offset. Fix: a date-only type.
  • String parsing in JavaScript. new Date("2026-03-05") is parsed as midnight UTC, while new Date("2026-03-05T00:00") is midnight in the browser's zone. One character changes the result.
  • Zero-based months. In the JS Date, January is 0.
  • "Plus 24 hours" instead of "plus 1 day". On a DST transition day, a day lasts 23 or 25 hours.
  • Server time. The server runs in a different zone than the developer, and the "today" report does not start at midnight. Keep servers and databases in UTC.
  • Comparing strings with different offsets. 12:30+05:00 and 07:30Z are the same instant but not equal strings.

How to do it in code

Displaying in a given zone in JavaScript uses the built-in Intl:

const d = new Date("2026-03-05T07:30:00Z");
new Intl.DateTimeFormat("en-GB", {
  timeZone: "Asia/Tashkent",
  dateStyle: "medium",
  timeStyle: "short",
}).format(d); // 5 Mar 2026, 12:30

For arithmetic and formatting, date-fns with date-fns-tz (formatInTimeZone, fromZonedTime) works well. Python has the standard zoneinfo module:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

now = datetime.now(timezone.utc)               # instant in UTC
local = now.astimezone(ZoneInfo("Asia/Tashkent"))
print(local.isoformat())

Avoid "naive" datetime objects without tzinfo: they cannot be compared correctly with aware values.

Checklist for APIs and databases

  1. Store instants in a zone-aware type (in PostgreSQL, timestamptz) and keep the connection session in UTC.
  2. Store calendar dates in a date type, not timestamp.
  3. The API accepts and returns ISO 8601 with Z or an offset; reject strings without one.
  4. For schedules, store local time and the IANA zone name as separate fields.
  5. Take the user's zone from their profile or the browser (Intl.DateTimeFormat().resolvedOptions().timeZone), not from guessing by IP.
  6. Compute period boundaries ("per day", "per month") in the user's zone, then convert to UTC for the query.
  7. Write date tests around DST transitions and month ends.
  8. Do not store Unix time in seconds in one place and milliseconds in another; agree on one.

FAQ

Users, servers and external services will end up in other zones over time. UTC in storage costs nothing now, while migrating data later is expensive.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.