Developer

Timestamp Conversion: Unix Time, ISO 8601, and the Time Zone Problems Nobody Warns You About

Time zones, daylight saving, leap seconds, the Year 2038 problem. Learn the formats, the gotchas, and the rules that make timestamps reliable across systems.

12 min read

Most date bugs come from the same handful of mistakes: storing local time, comparing strings instead of numbers, ignoring daylight saving, or treating "the day a user clicked save" as a meaningful global concept. Learning what to store, what to display, and what to compare prevents 90% of them.

Three formats you'll encounter

1. Unix timestamp (epoch time)

Number of seconds since 1970-01-01 00:00:00 UTC. Sometimes in milliseconds (JavaScript), microseconds (some databases), or nanoseconds (Go).

  • 1730000000 = Oct 27, 2024 02:13:20 UTC
  • Best for storing & comparing — single number, no time zone confusion.
  • Worst for human reading — opaque without conversion.

2. ISO 8601

Human-readable but unambiguous: 2024-10-27T02:13:20Z. The Z means UTC; an offset like +05:30 indicates a specific time zone.

  • Best for APIs and logs — unambiguous, sortable as strings.
  • Always include the time zone (Z or offset). Never store as "naive" local time.

3. RFC 2822 / Email Date format

Sun, 27 Oct 2024 02:13:20 GMT. Common in HTTP headers and email. Avoid for new systems.

The cardinal rules

  1. Store everything in UTC. Convert to local time only at display.
  2. Use Unix timestamps for storage and comparison; ISO 8601 for transit.
  3. Never compare or sort time zone-naive local times. "9 AM" means different things in different zones.
  4. Always specify the time zone when displaying. "3:00 PM ET" is clear; "3:00 PM" is ambiguous.

The time zone is not just an offset

America/New_York is sometimes UTC-5 (EST) and sometimes UTC-4 (EDT) — the offset changes twice a year for daylight saving. Store the IANA time zone name, not the offset, if you need to display future events correctly.

Example: a meeting scheduled for "3 PM in New York on March 15, 2027." If you stored the offset as UTC-5, but DST starts on March 8, 2027, your event would display at the wrong time. Store "America/New_York" and let the time-zone library compute the offset at display time.

The DST gotcha

On the "spring forward" day, 2:00–3:00 AM doesn't exist. On "fall back", 1:00–2:00 AM happens twice. If your system processes scheduled events "every minute," you can either skip 60 minutes of work or do it twice. Use UTC to schedule and you're immune.

The JavaScript Date trap

JavaScript's built-in Date object has notorious quirks:

  • new Date('2024-10-27') is parsed as UTC midnight (ISO interpretation).
  • new Date('2024/10/27') is parsed as local midnight.
  • new Date('10/27/2024') is parsed as US-style local; ambiguous in many locales.
  • Months are 0-indexed: new Date(2024, 9, 27) = October 27.

Use a real library for non-trivial work: date-fns (functional, tree-shakable),Luxon (immutable, full TZ support), or Temporal (the upcoming standard).

The Year 2038 problem

32-bit signed integer Unix timestamps overflow on January 19, 2038 at 03:14:07 UTC. Any system using a 32-bit time_t will report dates after this as 1901 or fail outright.

Most modern systems have moved to 64-bit timestamps (good for ~292 billion years). But:

  • Embedded systems, legacy databases, and some financial systems still use 32-bit.
  • MySQL TIMESTAMP column is 32-bit. Use DATETIME for dates beyond 2038.
  • Some cookies and HTTP cache headers are still 32-bit.

Microseconds and high-precision time

For high-frequency systems (financial trading, distributed systems, observability), seconds aren't enough. Common precisions:

  • Milliseconds: JavaScript's default. Most web APIs.
  • Microseconds: PostgreSQL's default for timestamp. Linux gettimeofday().
  • Nanoseconds: Go's time.Time. Modern clock_gettime().

When converting between systems, watch for unit mismatches. A "timestamp 1730000000" in seconds is October 2024; in milliseconds, it's January 1970.

Common bugs and their fixes

Bug: dates off by hours

Server stores in UTC, client displays in local time, but conversion code mixes them. Make conversion explicit at the display boundary, not in business logic.

Bug: events "disappearing" on certain days

Storing dates as YYYY-MM-DD strings and parsing them as JavaScript Dates causes them to appear as "previous day" in negative-UTC time zones. Use date-only types (PostgreSQL date, Java LocalDate, etc.) or treat date strings as opaque.

Bug: filtering "today" returns wrong rows

"Today" depends on the time zone. A user in Tokyo and a user in San Francisco are looking at different days for 7+ hours each day. Define "today" in the user's time zone, not the server's.

Bug: scheduled jobs running at the wrong time during DST

Cron schedules in "local" time can skip or repeat hours during DST transitions. Always run cron in UTC (or a fixed-offset zone like UTC+9 for Tokyo) and convert if needed.

Database storage best practices

  • PostgreSQL: use TIMESTAMPTZ, not TIMESTAMP. Stores in UTC, displays in session time zone. TIMESTAMP is "naive" — store at your peril.
  • MySQL: DATETIME for dates after 2038, TIMESTAMP auto-converts to UTC and back to session zone. Pick one and document.
  • SQLite: no native timestamp type — use ISO 8601 text or Unix integer.
  • MongoDB: Date type stores in UTC milliseconds. Always convert to UTC before insert.

Client-side display

Modern browsers expose the user's time zone via Intl.DateTimeFormat().resolvedOptions().timeZone. Use it to convert UTC server timestamps for display, but consider letting users override it (for shared team contexts where everyone needs to see the same time).

Key Takeaways

  • Store in UTC. Convert to local time only at display. Never store time zone-naive local times.
  • Use Unix timestamps for comparison/storage; ISO 8601 (with Z or offset) for APIs.
  • Time zones aren't fixed offsets — DST changes them. Store IANA names like 'America/New_York' for future events.
  • JavaScript Date has known quirks: parse with care, prefer date-fns/Luxon/Temporal for non-trivial work.
  • Year 2038 problem still affects legacy systems. MySQL TIMESTAMP overflows; use DATETIME for dates beyond 2038.

Convert any timestamp instantly using our Timestamp Converter.