Time Zones for Developers: UTC, IANA, DST and How to Handle Them
Time zones are the kind of problem that look trivial until they have eaten three sprints. A meeting reminder fires an hour late, a daily cron skips a day, a report shows yesterday's revenue split across two days. Every one of those bugs comes from the same root mistake: treating local time as if it were a stable thing. It is not. This guide walks through the rules that actually keep date handling correct across timezones, from the database row up to the rendered string.
The Golden Rule: Store UTC
Every timestamp in your database, log file, message queue, and API payload should be UTC. UTC has no offset, no daylight saving, no political boundary that can shift it. It is the one timezone the entire system can agree on. Local time only enters the picture at the very last step, when a human is about to see the value.
The format you want is ISO 8601 with the trailing Z: 2026-05-13T14:30:00Z. The Z literally means "Zulu", which means UTC. Avoid storing strings like 2026-05-13 14:30:00 without an offset. You will never reliably know what zone that meant six months from now.
UTC, GMT, and Offsets Are Not the Same
- UTC is Coordinated Universal Time. It is a precise atomic-clock-based standard with no offset. It never observes daylight saving.
- GMT is Greenwich Mean Time, the historical predecessor. In casual use it overlaps with UTC, but technically GMT is a timezone (Europe/London in winter) and UTC is a standard.
- Offsets like
+05:30or-08:00describe the difference from UTC at a specific instant. They are not timezones. The same physical location has different offsets in summer and winter. - Timezones are named rules like
America/Los_AngelesorAsia/Kolkata. They map a wall-clock time at a given location to a UTC instant, with all the daylight-saving and historical shifts baked in.
The IANA tz Database
The Internet Assigned Numbers Authority maintains the canonical list of timezones. Every operating system, language runtime, and database that handles time correctly ships a copy of this database. It contains roughly 600 zones with names like Europe/Istanbul, Pacific/Auckland, and America/Argentina/Buenos_Aires.
Always use IANA names, never three-letter abbreviations. EST is ambiguous (Eastern Standard Time in North America? Australian Eastern Standard Time?), and it does not encode daylight saving rules. America/New_York is unambiguous and shifts between EST and EDT automatically.
Daylight Saving Is Where Bugs Live
DST is the source of most production date bugs. When clocks move forward in spring, one hour of local time simply does not exist. When they fall back in autumn, one hour repeats. Any code that assumes "one day = 24 hours" or "add one hour to get the next slot" will misbehave twice a year.
Concrete failure modes you should design around:
- Non-existent local times. 2:30 AM on the spring-forward day in New York does not exist. If a user schedules a recurring meeting at 2:30 AM local, you have to decide whether to fire it at 1:30 or 3:30 that day.
- Ambiguous local times. 1:30 AM on the fall-back day happens twice. A timestamp like
2026-11-01T01:30:00inAmerica/New_Yorkmaps to two different UTC instants. - 23-hour and 25-hour days. Code that says
date.setHours(date.getHours() + 24)can skip or repeat a day across a DST boundary. - Government rule changes. Countries add, remove, or shift DST regularly. Brazil removed DST in 2019. Mexico mostly removed it in 2022. Your tz database needs regular updates or your future dates will drift.
JavaScript: Use Intl and Temporal
The built-in Date object is a UTC timestamp wrapped in a confusing local-time API. For display, use Intl.DateTimeFormat, which is timezone-aware and ships with every modern browser and Node version.
const utc = new Date("2026-05-13T14:30:00Z");
new Intl.DateTimeFormat("en-US", {
timeZone: "America/Los_Angeles",
dateStyle: "medium",
timeStyle: "short",
}).format(utc);
// "May 13, 2026, 7:30 AM"
new Intl.DateTimeFormat("tr-TR", {
timeZone: "Europe/Istanbul",
dateStyle: "long",
timeStyle: "short",
}).format(utc);
// "13 Mayıs 2026 17:30"For arithmetic, prefer the new Temporal API, which separates plain dates, zoned date-times, and instants into distinct types and refuses to silently coerce them.
const zoned = Temporal.ZonedDateTime
.from("2026-03-08T01:30:00-05:00[America/New_York]")
.add({ hours: 2 });
// Crosses the DST boundary correctly.
zoned.toString();
// "2026-03-08T04:30:00-04:00[America/New_York]"Database Patterns
- PostgreSQL: use
timestamptz(timestamp with time zone), nottimestamp. Despite the name,timestamptzstores a UTC instant and converts on read. - MySQL: use
DATETIMEwith all writes already converted to UTC, or useTIMESTAMPand pintime_zone = 'UTC'at the session level. MySQLTIMESTAMPonly covers 1970 to 2038, so it is increasingly limiting. - SQLite: store ISO 8601 strings ending in Z. SQLite has no native date type.
- When the user's zone matters semantically (recurring schedules, business hours, calendar events), store the IANA zone name in a separate column alongside the UTC timestamp. Reconstruct the local interpretation at read time.
Storing Wall-Clock Recurring Events
A "9:00 AM every weekday" reminder is not a UTC instant. It is a local wall-clock time that survives DST changes. If you store it as a UTC timestamp and add 24 hours each day, your 9:00 AM reminder drifts to 8:00 AM after the spring-forward.
Store these as a tuple of (local time, IANA zone, recurrence rule). Resolve to a UTC instant at scheduling time, not at storage time.
Common Mistakes
- Trusting the user's browser timezone for server-side logic. Send it as data, treat it as a hint, but rerun any business-critical decision on the server with the user's configured zone.
- Using offsets in place of zones.
+02:00is not Berlin. It is "wherever happens to be on +02:00 right now". Berlin moves to +01:00 in winter. - Concatenating strings to "convert". Sticking
+00:00onto a local time does not turn it into UTC. It just lies about what zone it was in. - Forgetting to update the tz database. Most production bugs after a country shifts DST rules come from outdated tz data baked into a container image from years ago.
- Rendering with the server's zone. The server runs in UTC, the user lives in Istanbul. Format on the client or pass the zone explicitly.
TL;DR
- Store UTC, always. Format to local only at the rendering edge.
- Use ISO 8601 with the Z suffix. Never store local strings without an offset.
- Use IANA zone names like
America/New_York, never abbreviations like EST. - Use
Intl.DateTimeFormatorTemporalin JavaScript. Do not roll your own. - For recurring wall-clock events, store (local time + IANA zone), not just a UTC instant.
- Keep your tz database updated.
Convert Unix timestamps to any timezone
Use our free Timestamp Converter to translate UTC, local time, and Unix epoch values across IANA zones. Spot-check API payloads, debug DST edge cases, and copy ISO 8601 strings in one click.
Open Timestamp Converter