Toolbit
Guides

Unix Timestamps and Time Zones: Handling Dates Without Bugs

The difference between an instant, local time, offset, and time zone, what daylight saving time does, what to store in your database, and the classic bugs in JavaScript, Python, and SQL.

By Javier VallejoPublished 6 min read

Two ideas that get mixed up

Nearly every date bug comes from blending two different concepts:

  • An instant is a single point on the timeline, the same for the entire planet. "The moment this payment was recorded" is an instant. A Unix timestamp like 1759672800 represents an instant.
  • A wall clock time is what a clock shows in a particular place: "October 5 at 9:00 AM." On its own it doesn't identify an instant, because 9:00 AM in Chicago and 9:00 AM in London are hours apart.

To go from a wall clock time to an instant, you need to know which zone that clock is in. To go from an instant to a wall clock time, you need to know which zone you're displaying it for. This guide covers how to get both conversions right, what daylight saving time does to them, and what you should actually store in a database. If you just need to convert a specific value, the Unix timestamp converter does both directions right in your browser.

UTC, offset, and time zone are not the same thing

These three terms get used interchangeably, and they shouldn't be:

TermWhat it isExample
UTCThe worldwide reference time scale. It has no daylight saving time.2025-10-05T14:00:00Z
OffsetHow far a local clock is ahead of or behind UTC at a given instant.-05:00, +01:00
Time zoneA place's rules: which offset it uses on every date, including past and future clock changes.America/Chicago, Europe/London

The offset-versus-zone distinction is the one that matters most. America/New_York is -05:00 in winter and -04:00 in summer; knowing a time has a -05:00 offset doesn't tell you whether it's New York in winter, Chicago in summer, or Bogotá year-round. Zones are identified by names from the IANA time zone database (Continent/City), which is what operating systems, browsers, and nearly every language use.

Abbreviations like CST or IST are best avoided entirely: CST can mean US Central Standard Time (UTC−6), China Standard Time (UTC+8), or Cuba Standard Time (UTC−5); IST can be India, Ireland, or Israel.

Daylight saving time: hours that don't exist and hours that happen twice

Twice a year, in zones that observe daylight saving time, the mapping between wall clock time and instants stops being one-to-one. In the United States, clocks change at 2:00 AM local time on the second Sunday of March and the first Sunday of November. In New York in 2026:

DateWhat happensConsequence
March 8, 2026At 2:00 AM the clock jumps to 3:00 AMTimes from 2:00 to 2:59 AM don't exist that day
November 1, 2026At 2:00 AM the clock falls back to 1:00 AMTimes from 1:00 to 1:59 AM happen twice

On November 1, "1:30 AM in New York" maps to two different instants: 2026-11-01T01:30-04:00 (timestamp 1793511000) and, an hour later, 2026-11-01T01:30-05:00 (timestamp 1793514600). Without the offset, the local time is ambiguous. On March 8, on the other hand, "2:30 AM in New York" maps to no instant at all, and each library handles that its own way — some roll forward to 3:30, others throw an error.

That has practical consequences:

  • A day isn't always 24 hours. Changeover days last 23 or 25 hours. Adding 86400 seconds to a timestamp is not the same as "same time tomorrow" if there's a clock change in between; for that, you add one calendar day in the zone.
  • Jobs scheduled in that window misbehave. A 2:30 AM cron job may be skipped or run twice depending on the implementation. Crontab: 20 Real-World Examples Explained covers how to steer clear of it.
  • Hourly reports get a gap or a double hour if they group by local hour instead of UTC hour.

What to store in the database

The general rule has two halves, depending on whether the event already happened or is scheduled for the future.

Events that already happened (a payment, a login, a log line): store the instant, in UTC. It's immutable — the payment happened at that moment, and no future time zone rule will change that. In PostgreSQL, the right type is timestamptz (which stores an instant and displays it converted to the session's time zone), not plain timestamp (which stores a wall clock time with no zone). In MySQL, DATETIME with a strict always-UTC convention is safer than TIMESTAMP, whose range ends in 2038.

Future events tied to local time (a standing Monday 10:00 AM meeting in Denver, a store that closes at 9:00 PM): store the wall clock time and the IANA zone, and compute the instant when you need it. The reason is that time zone rules change by political decision, sometimes with only weeks of notice. Mexico abolished daylight saving time across most of the country in October 2022; the US has repeatedly debated making it permanent. If a 2027 meeting was saved as a UTC instant under the old rules, it will show up at the wrong hour after the rules change.

Classic bugs in code

JavaScript: date-only is UTC, date-time is local. Per the spec, new Date("2026-10-05") is parsed as midnight UTC, while new Date("2026-10-05T00:00") is parsed as midnight local time. Anywhere in the Americas, the first one displays as October 4. On top of that, months are zero-based in the numeric constructor: new Date(2026, 9, 5) is October 5.

JavaScript: seconds versus milliseconds. Date works in milliseconds, while most APIs return seconds. new Date(1759672800) gives a date in January 1970; the correct call is new Date(1759672800 * 1000).

Python: naive datetimes. A datetime without tzinfo has no idea what zone it's in, and Python treats it as server-local time in some operations and as UTC in others. datetime.utcnow() returns exactly one of those zone-less objects and has been deprecated since Python 3.12. The right way is datetime.now(timezone.utc) for the current instant and zoneinfo.ZoneInfo("America/Chicago") for working with zones.

Comparing dates as strings. "2025-10-05T09:00-05:00" and "2025-10-05T14:00Z" are the same instant, but as strings they aren't equal and don't even sort correctly against each other. Normalize to UTC or to a timestamp before comparing or sorting.

Inferring the zone from the browser's offset. getTimezoneOffset() returns today's offset, not the zone. To get the user's zone, use Intl.DateTimeFormat().resolvedOptions().timeZone, which returns the IANA name.

A complete example

An app needs to show a user in Los Angeles when an event recorded with timestamp 1759672800 happened:

  1. The timestamp is an instant: 2025-10-05T14:00:00Z.
  2. The user's zone is America/Los_Angeles. In early October it's still on daylight time, so the offset is −07:00.
  3. The wall clock time is 2025-10-05 07:00.

That same instant, shown to a user in London (Europe/London, on British Summer Time in early October, +01:00), is 2025-10-05 15:00. You can check all three representations by loading timestamp 1759672800 into the converter and switching the zone.

Checklist

  1. Servers, databases, and logs on UTC. Convert to local time only for display, at the edge (the UI or the report).
  2. Past instants as UTC; future events as local time + IANA zone.
  3. Zones by IANA name — never by abbreviation or fixed offset.
  4. ISO 8601 with an offset or Z for every text exchange (APIs, CSV, JSON). A date without a zone is an ambiguous date.
  5. Zone-aware libraries: Intl and Temporal in JavaScript, zoneinfo in Python, java.time in Java. Keep the OS time zone data (tzdata) up to date.
  6. Tests that cross a clock change. Time zone bugs don't show up in tests that run on an ordinary Tuesday.

Summary

A timestamp is an instant; a local time is an instant seen from a zone. Converting between the two takes the IANA zone, not just the offset, because offsets shift with daylight saving time and with each country's decisions. Store instants in UTC, store future appointments with their zone, convert to local time only for display — and most date bugs disappear.

Tools used in this guide

Related guides