Skip to main content
Technical

Time Zones: A Developer's Survival Guide

personWritten by Magnus Silverstream
calendar_todayMarch 6, 2026
schedule8 min read

Time zones are one of the few areas in software development where the rules are literally decided by politicians and can change with a few months' notice. A country decides to stop observing daylight saving time, a region shifts its UTC offset, or a territory splits into two zones — and suddenly your application shows the wrong time to millions of users. This guide covers the practical knowledge every developer needs to handle time zones correctly.

UTC: the universal reference point

UTC (Coordinated Universal Time) is the time standard that all time zones are defined relative to. It does not observe daylight saving time and never changes. Key facts: • UTC replaced GMT (Greenwich Mean Time) as the global standard in 1960 • UTC is maintained by atomic clocks and is adjusted with leap seconds to stay in sync with Earth's rotation • All IANA timezone identifiers (e.g., America/New_York) define their offset relative to UTC • UTC+0 covers countries like the UK (in winter), Iceland, Portugal, and several West African nations UTC offsets: • Offsets range from UTC-12 (Baker Island) to UTC+14 (Line Islands, Kiribati) • Yes, UTC+14 exists — some Pacific islands are a full day ahead of UTC-12 • Some offsets are not whole hours: India uses UTC+5:30, Nepal uses UTC+5:45, the Chatham Islands use UTC+12:45 Why UTC matters for developers: • Store all timestamps in UTC — this is the single most important rule • Convert to local time only at the display layer, as close to the user as possible • Comparing UTC timestamps is a simple numeric comparison with no ambiguity • Database queries, log analysis, and cron jobs all become simpler when everything is in UTC

The daylight saving time trap

Daylight Saving Time (DST) is the single largest source of timezone-related bugs. Rules vary by country, change frequently, and create edge cases that are difficult to test. How DST breaks things: • When clocks "spring forward," one hour is skipped — 2:00 AM jumps to 3:00 AM. Any event scheduled at 2:30 AM does not exist. • When clocks "fall back," one hour repeats — 1:30 AM occurs twice. A timestamp of 1:30 AM is ambiguous. • DST transitions happen at different times in different countries — the US and EU switch on different weekends • Some countries within the same UTC offset observe DST while others don't (Arizona vs the rest of US Mountain Time) Recent changes: • The EU voted to abolish DST (proposed for 2021, still pending as of 2026) • Turkey stopped observing DST in 2016 and permanently moved to UTC+3 • Morocco has switched DST policies multiple times in recent years • Chile and Argentina have changed their DST rules repeatedly DST bugs in the wild: • Calendar apps showing meetings at the wrong time after a transition • Scheduled tasks (cron jobs) running twice or not at all during the transition hour • Billing systems charging for 23 or 25 hours on transition days • Analytics dashboards showing spikes or dips on transition days due to incorrect aggregation Rule: never hardcode DST rules. Always rely on the IANA timezone database (tzdata), which is updated multiple times per year.

Storing time in databases

How you store time in your database determines how much pain you'll experience when your application serves users across multiple time zones. Recommended approaches: PostgreSQL: • Use TIMESTAMP WITH TIME ZONE (timestamptz) — stores as UTC internally, converts on input and output based on the session timezone • Avoid TIMESTAMP WITHOUT TIME ZONE — it stores whatever you give it with no timezone awareness, leading to ambiguity • Set your database timezone to UTC: SET timezone = 'UTC'; MySQL: • DATETIME stores the literal value with no timezone conversion — dangerous if servers are in different zones • TIMESTAMP converts to UTC on storage and back to the session timezone on retrieval — generally safer • TIMESTAMP has a range limit (1970-2038) due to its 32-bit Unix timestamp backing • For new projects, use DATETIME with application-level UTC enforcement, or BIGINT for Unix timestamps MongoDB: • Date type stores as UTC milliseconds internally — handles timezone correctly by default • Always pass UTC dates when inserting — avoid new Date() on a server with a non-UTC timezone setting General rules: • Always store in UTC. No exceptions. • Include timezone information in the column name (created_at_utc) or documentation if the database type doesn't enforce it • Never rely on the database server's local timezone setting — it can change during deployments or migrations • Store the user's preferred timezone as a separate field (e.g., user_timezone = 'America/Toronto') for display purposes

Client-side time display

The browser is where UTC timestamps should be converted to the user's local time. Modern JavaScript provides built-in tools for this. The Intl.DateTimeFormat API: • Built into all modern browsers — no library needed • Automatically handles the user's locale and timezone • Supports extensive formatting options Examples: • Basic: new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(date) • With timezone: new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', timeStyle: 'long' }).format(date) • Relative time: Intl.RelativeTimeFormat for "3 hours ago" style display Detecting the user's timezone: • Intl.DateTimeFormat().resolvedOptions().timeZone returns the IANA timezone (e.g., 'America/Toronto') • This is more reliable than calculating the offset, because it accounts for DST automatically • Store this value to display times correctly even when rendering server-side Common mistakes: • Using getTimezoneOffset() for display — this returns minutes, is inverted (negative = east of UTC), and doesn't give you the IANA name • Formatting dates on the server in a specific timezone and sending HTML — this breaks for users in other zones • Using toLocaleString() without specifying a timezone — the result depends on the server's locale in SSR contexts Best practice: • Send UTC timestamps (ISO 8601 or Unix) from your API • Let the client format using Intl.DateTimeFormat with the user's timezone • For server-rendered pages, detect timezone via a cookie or JavaScript on first visit, then render correctly on subsequent requests

Libraries and tools

While native APIs cover many use cases, some scenarios benefit from dedicated libraries. date-fns-tz: • Lightweight companion to date-fns for timezone operations • Functions: formatInTimeZone, toZonedTime, fromZonedTime • Tree-shakeable — only import what you use • Uses the IANA timezone database bundled in the browser Luxon: • Full-featured DateTime library built by a Moment.js maintainer • First-class timezone support: DateTime.now().setZone('America/New_York') • Handles DST transitions and ambiguous times explicitly • Slightly larger bundle size than date-fns-tz Temporal (upcoming standard): • A proposed JavaScript standard for modern date/time handling • ZonedDateTime type that carries timezone information alongside the timestamp • Eliminates most of the pitfalls of the current Date object • Available now via polyfills, expected in browsers in the near future For backend: • Python: use zoneinfo (standard library since 3.9) or pytz. Always use aware datetimes (with timezone), never naive ones. • Java: use java.time (ZonedDateTime, Instant) — never use java.util.Date • Go: time.LoadLocation("America/New_York") — the standard library handles timezones well • Rust: chrono crate with chrono-tz for IANA timezone support Keep tzdata updated: • The IANA timezone database is updated several times per year • Your operating system and programming language runtime need regular updates to reflect changes • Cloud providers generally keep their base images updated, but verify after deploying to new instances

Common bugs and how to avoid them

These are the bugs that every developer encounters at least once. Recognizing the patterns saves hours of debugging. Bug: event shows at the wrong time after DST transition • Cause: storing local time instead of UTC, so the offset changes but the stored value doesn't • Fix: store UTC, convert to local only at display time Bug: user creates event at 2:30 AM on spring-forward day, and it disappears • Cause: 2:30 AM doesn't exist during spring-forward — the clock jumps from 2:00 to 3:00 • Fix: validate the local time when the user selects it. If it falls in the skipped hour, prompt the user to choose an adjacent time. Bug: cron job runs twice on fall-back day • Cause: the cron scheduler sees 1:30 AM occur twice and triggers the job both times • Fix: use UTC for cron schedules, or use a scheduler that tracks whether a job has already run in the current calendar day Bug: "created 1 hour ago" shows as "created 2 hours ago" for some users • Cause: the server calculates relative time using its own timezone instead of the user's • Fix: send the UTC timestamp to the client and compute relative time in the browser using Intl.RelativeTimeFormat Bug: database query returns wrong results for "today's orders" • Cause: the query uses the database server's timezone for "today" boundaries, which differs from the user's timezone • Fix: pass the start and end of "today" as UTC timestamps from the application, based on the user's timezone Bug: tests pass locally but fail on CI • Cause: the CI server uses UTC while the developer's machine uses local time, and the tests compare formatted dates • Fix: set TZ=UTC in your CI environment and write tests that explicitly specify timezone context

ISO 8601: the format to use everywhere

ISO 8601 is the international standard for date and time representation. When in doubt, use it. Format: • Date: 2024-03-08 • Date and time in UTC: 2024-03-08T16:00:00Z • Date and time with offset: 2024-03-08T11:00:00-05:00 • Duration: P1Y2M3DT4H5M6S (1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds) • Week: 2024-W10 (week 10 of 2024) Why ISO 8601: • Unambiguous — 03/08/2024 could be March 8 or August 3 depending on locale; 2024-03-08 is always March 8 • Sortable — lexicographic string sorting puts ISO 8601 dates in chronological order • Machine-parseable — every major programming language has built-in ISO 8601 parsing • Human-readable — unlike Unix timestamps, you can glance at it and know the date Where to use it: • API request and response bodies • Log files and structured logging • File names for date-based files: report-2024-03-08.csv • User-facing displays (with locale-appropriate formatting applied on top) Where NOT to use it: • HTTP headers — these use RFC 2822 format per the HTTP specification • Filenames on Windows — colons in the time portion (16:00:00) are invalid in file paths. Use 2024-03-08T160000Z instead. • User input fields — let users enter dates in their local format and convert to ISO 8601 internally

Conclusion

Time zones will never be simple, but following a few consistent rules — store in UTC, convert at the display layer, rely on the IANA database, and never hardcode offsets — prevents the vast majority of bugs. When you need to quickly convert between time zones or verify a timestamp, use our timestamp converter to see any time in any zone instantly.

Frequently Asked Questions

Yes. Storing in UTC eliminates ambiguity, makes comparisons straightforward, and avoids issues when daylight saving rules change. Convert to local time only at the display layer, as close to the user as possible. The only exception is when you need to preserve the user's intended local time (e.g., a reminder set for '9 AM' regardless of timezone changes).