Time looks like the simplest data type in your system. It is the one that ships the most silent, hard-to-reproduce bugs. Here are the seven that catch nearly every team.

Time is the data type that looks trivial and isn't. A datetime is just a number on a clock — until you ship it to users in another country, run it through a daylight-saving transition, or store it on a server that quietly runs on a different timezone than your laptop. Then the "number on a clock" turns out to be an ambiguous, politically-mutable, occasionally-nonexistent value, and your reminders fire an hour late, your reports double-count one day, and one customer swears an event happened before it was created.

Almost every timezone bug traces back to one of seven wrong assumptions. Here they are, each with the specific failure it produces and the fix. The good news: the fixes converge on three rules — store UTC, use timezone-aware datetime libraries, and identify zones by IANA name rather than raw offset.

Gotcha 1: Storing local time instead of UTC

The original sin. A user in Rome creates an event "at 14:00" and the app stores 14:00. No timezone attached. Six months later a colleague in New York opens it, or the value gets compared against a server clock, and there is no way to know what instant 14:00 actually referred to. The string is not a point in time; it is a point in time plus a missing piece of context.

The fix is the one rule that prevents most of the bugs below: store instants in UTC. Convert to the user's local zone only at the moment of display. UTC never observes daylight saving, never shifts with politics, and gives every stored instant one unambiguous meaning. Keep the user's chosen zone as a separate field (an IANA name — see Gotcha 7) when you need to re-render local wall-clock time later.

Gotcha 2: DST transitions create times that don't exist — and times that happen twice

Daylight saving is where "wall-clock time" stops being a well-behaved number. Twice a year the local clock jumps:

The fix is to never do arithmetic on local wall-clock times across a transition. Work in UTC, where the underlying instants are continuous and monotonic, and let a timezone-aware library map to and from local time. Good libraries expose the ambiguity explicitly — Python's zoneinfo, for instance, offers a fold attribute to distinguish the first 02:30 from the second, and will tell you when a wall-clock time falls in a gap instead of guessing.

Gotcha 3: Assuming offsets are fixed

A tempting shortcut is to store "the user is UTC+1" and reuse that number forever. Offsets are not constants. They change with the seasons (DST), and they change with politics: governments move zones, adopt or abolish daylight saving, and redraw boundaries with little notice. Samoa skipped December 30, 2011 entirely to switch sides of the date line. Multiple regions have proposed or enacted permanent-DST changes in recent years. An offset you hard-coded last year can simply be wrong this year.

The fix: do not store offsets as identity. An offset like +01:00 is a fact about one instant, not a property of a place. Store the IANA zone name (Europe/Rome) and derive the offset for a given instant at compute time, using an up-to-date tz database. When the rules change, you update the database, not your data.

Gotcha 4: Off-by-one from date-only vs datetime

A "date" like 2026-08-21 has no time and, strictly, no zone — but code constantly treats it as if it were midnight somewhere. The classic bug: a date-only value gets interpreted as 2026-08-21T00:00:00 in UTC, then displayed in a timezone west of UTC, where that instant is still 2026-08-20. Birthdays land a day early, "day of" reports drift, invoices book to the wrong period.

The fix is to keep date-only values as dates and never silently promote them to instants. When a boundary genuinely matters — "everything that happened on the 21st in Rome" — compute the start and end instants explicitly in the relevant zone (midnight Europe/Rome to midnight the next day), convert those to UTC, and query the half-open range. Be deliberate about which zone defines "a day"; it is a business decision, not a default.

Gotcha 5: Using the server's local time

Code that calls the equivalent of now() without specifying a zone gets whatever the host is configured to. That works on your machine, passes in CI, and breaks the day the workload moves to a container, a serverless region, or a colleague's laptop in another country. Timestamps written by two instances of the same service can disagree by hours, and logs stop being comparable.

The fix: make the server's timezone irrelevant. Configure hosts to UTC where you can, but more importantly, always ask for the current time in UTC explicitly and never rely on the ambient local zone. The correctness of your timestamps should not depend on a machine's regional settings.

Gotcha 6: Naive vs aware datetimes

Most languages distinguish a naive datetime (a wall-clock reading with no zone) from an aware one (an instant that knows its offset/zone). Naive datetimes are the raw material of nearly every bug above, because two of them can be compared or subtracted while representing instants hours apart — and the language will happily give you a wrong answer with no error.

The fix: make datetimes aware at the boundary and keep them aware. The moment a value enters your system — parsed from input, read from a row, returned by an API — attach its zone (usually UTC). Treat a naive datetime crossing a module boundary as a bug, the way you would treat an un-parsed string being passed where a number is expected.

Gotcha 7: UTC is not just "+00:00" — use IANA names, not offsets

Two subtle points sit under everything above. First, UTC is a time standard, not merely an offset. A timestamp written as +00:00 shares UTC's offset but carries no daylight-saving rules; UTC itself never has any. (UTC also historically inserts leap seconds to stay aligned with astronomical time — which is why 23:59:60 is a legal, if rare, wall-clock value. Most application stacks smear or ignore leap seconds, but it is worth knowing the second count of a UTC day is not always 86,400.)

Second, and most important operationally: identify zones by IANA name, not by offset. The IANA Time Zone Database (also called the tz database or "Olson" database) is the canonical, regularly-updated record of every zone's rules and their history — past and scheduled offset changes, DST rules, the lot. A name like Europe/Rome encodes all of that. An offset like +02:00 encodes none of it and cannot survive the next rule change. Store names; derive offsets; update the database when governments legislate.

Instead of Store / use Why
Local wall-clock stringUTC instantOne unambiguous meaning
Fixed offset "+01:00"IANA name "Europe/Rome"Survives DST and law changes
Naive datetimeAware datetimeComparisons stay correct
Server local now()Explicit UTC nowHost-independent
Bare date treated as midnight UTCExplicit zone-scoped rangeNo off-by-one day

Tool walkthrough

When you are debugging one of these bugs, the fastest first step is to pin down what an instant actually is in each zone. Toolhub's timezone converter takes a wall-clock time in one IANA zone and shows the corresponding local time in others, applying the correct DST rules for the date you pick — so you can see for yourself that a spring-forward 02:30 has no valid mapping, or that a fall-back window resolves two ways. To move between human-readable datetimes and the underlying UTC epoch value your logs and databases store, the timestamp converter turns a Unix timestamp into a readable UTC datetime and back, which is the quickest way to confirm whether a stored value really is the instant you think it is. Used together, they let you check the assumption at the root of most of these gotchas: that everyone involved agrees on which instant a given reading refers to.

Where to read further

None of these gotchas require exotic knowledge to avoid. Store instants in UTC, keep every datetime timezone-aware from the boundary inward, and identify zones by IANA name so the tz database — not your hard-coded assumptions — carries the rules. Do that consistently and the whole class of "it worked on my machine, an hour off in production" bugs simply stops occurring.

← All articles