Time Zones in Code: Why This Is One of the Most Treacherous Topics in Software Development

Few areas of programming produce as many hidden bugs as working with time. Code that runs flawlessly during development, testing, and even for months in production suddenly breaks on a specific day of the year, for a specific user, in a specific region. The cause is almost always the same: somewhere in the logic, a developer explicitly or implicitly assumed that time is uniform and predictable, when in reality it’s one of the most non-linear systems you’ll ever work with in code.

This article isn’t about the syntax of any particular library. It’s about the mental model that helps you avoid the classic traps of working with time zones.

Time in code exists in two fundamentally different forms

The first thing worth separating clearly in your head: an instant in time and a civil, local representation of time are not the same thing, even though everyday language blurs the two constantly.

An instant in time, most often represented as a Unix timestamp, is a count of seconds (or milliseconds) since a fixed reference point, typically January 1, 1970, UTC. This number doesn’t depend on any time zone: it’s unambiguous anywhere on the planet.

A civil time representation, the familiar “September 15, 2026, 2:30 PM,” only makes sense within the context of a specific time zone. Without a zone attached, it’s just a set of digits that can be interpreted a dozen different ways.

Most real-world time bugs come from mixing these two concepts: storing civil time without a zone attached, then treating it as if it were an unambiguous instant.

Why storing local time without a zone is dangerous

A classic mistake: a database field like event_time, typed as a plain datetime, holds something like “2026-03-15 09:00:00” with no accompanying information about which zone it refers to. If the server, the application, and every user live in the same zone and never change location, this works fine. But the moment a single user shows up in a different region, or the server moves to a different data center, or the company expands internationally, this design starts producing bugs that are hard to reproduce locally.

The correct approach, long established as an industry standard, is to store the instant in UTC (usually as a timestamp or a datetime explicitly tagged as UTC), and perform the conversion to a user’s local time only at the display layer, as close to the interface as possible. The application logic, the database, and background jobs should operate exclusively in UTC; only the final step, showing a value to the user, converts that instant into a format they can understand.

For quickly checking what a given timestamp looks like across different zones during debugging, a ready-made tool like the Unix Timestamp Converter instantly shows the correspondence between a numeric value and a readable date in a chosen zone, without needing to write a throwaway script for a single check.

Why an hour offset, instead of a zone name, isn’t enough

Another common trap: storing not the name of the time zone (for example, Europe/London) but only a numeric offset from UTC (for example, +3). The problem is that the offset isn’t a constant: it changes twice a year in zones that observe daylight saving time, and even in zones without such transitions, the offset occasionally changes due to government decisions.

The IANA time zone database (the same one used under the hood by most modern programming languages, sometimes called the tz database or the Olson database) doesn’t just store the current offset. It stores the full history of rules for each region: exactly when daylight saving transitions happened in the past, when they’ll happen in the future, and how those rules have changed over the years. That’s why the correct practice is to store the zone name (Europe/London, America/New_York) rather than a bare numeric offset, and to let the appropriate library compute the actual offset for a given date, rather than writing that math by hand.

Daylight saving transitions: hours that don’t exist, and hours that exist twice

Daylight saving transitions create two counterintuitive situations that regularly break naive date logic.

The first is the “nonexistent” interval: when clocks spring forward, say from 2:00 AM straight to 3:00 AM, the interval from 2:00 to 2:59 that day simply doesn’t exist in that zone. If code tries to construct a date inside that interval, the behavior depends on the specific library: some automatically shift it forward, others throw an error, and others silently return an ambiguous result.

The second is the “duplicate” hour: when clocks fall back, a certain interval of time occurs twice within the same day. An event scheduled for “2:30 AM” on the day clocks fall back is ambiguous: it’s unclear whether it refers to the first or the second occurrence of that time.

These situations are especially dangerous for any logic involving recurring events: reminders, schedules, billing cycles. The most reliable way to avoid trouble is to define rules in terms of “when, in the user’s local time” (for example, “every Monday at 9:00 AM, Europe/London”), rather than trying to precompute and hardcode specific UTC instants years in advance. Computing the actual UTC instant for each individual occurrence should be done dynamically, using the current rules from the tz database.

Serialization and passing time between systems

When time data moves between services, between frontend and backend, or gets stored in files or message queues, the representation format becomes critically important. The ISO 8601 standard (for example, “2026-09-15T14:30:00Z” with an explicit UTC marker or offset) remains the safest choice for cross-system communication: it’s unambiguous, machine-readable, and supported natively by virtually every modern programming language.

Problems usually arise when one part of a system serializes time in a local format without a zone, while another part interprets the received value as UTC, or conversely, as its own local time. These mismatches are hard to spot during development, because in a test environment where everything runs in a single zone, the discrepancy simply never shows up.

Testing time-zone-sensitive logic

The fastest way to surface hidden problems in your code is to deliberately test it outside the developer’s usual zone. A good practice is running part of your test suite with the system zone forcibly set to something else, including zones with non-standard offsets (like the 30- or 45-minute offsets used in India or Nepal), and separately checking the code’s behavior on the actual dates of daylight saving transitions in the current and following years.

It helps to keep a simple reference tool on hand for manually sanity-checking assumptions while debugging or writing tests. The Time Tools section offers a set of calculators for converting time between zones and checking the current time in a specific region, useful as a quick “reality check” while investigating time-related bugs.

More generally, for any quick lookups of dates and times outside the context of your own project, it’s worth keeping a service like https://worldtimedata.com/ bookmarked, instead of manually computing zone differences or converting timestamps into a readable format by hand.

Practical rules worth baking into the architecture from day one

To sum up, a handful of principles substantially reduce the risk of time-zone bugs if they’re built into the architecture from the start of a project rather than patched in afterward.

Store the instant in UTC at the database and business-logic level, and convert to local time only at the point of display. Store the zone name (Region/City) rather than a bare numeric offset whenever you need to remember which zone a user or event belongs to. Trust offset and daylight-saving-rule calculations to well-tested libraries in your language, not hand-written math. Define recurring-event rules in terms of the user’s local time, not precomputed UTC instants. Use unambiguous serialization formats like ISO 8601 with an explicit zone marker for any cross-system time data. Test critical logic across multiple zones, including non-standard offsets and the actual dates of daylight saving transitions.

None of these rules is difficult on its own. The real problem is that ignoring them rarely causes immediate pain. Code built on implicit assumptions about time quietly works for months, until one day, most often right around a daylight saving transition or with the arrival of the first user in a non-standard zone, those assumptions turn out to be wrong. Getting the time model right at the start of a project is substantially cheaper than rewriting it after it has spread throughout the entire codebase.

Photo of author

Author

Dave

Hello, I'm Dave! I'm an Apple fanboy with a Macbook, iPhone, Airpods, Homepod, iPad and probably more set up in my house. My favourite type of mobile app is probably gaming, with Genshin Impact being my go-to game right now.

Read more from Dave

appsuk-symbol-cropped-color-bg-purple@2x

Apps UK
International House
12 Constance Street
London, E16 2DQ