unix time timestamps time zones debugging

Unix Timestamps: Seconds vs Milliseconds, Time Zones, and the 2038 Problem

How to read a Unix timestamp correctly — telling seconds from milliseconds, converting to any time zone, handling daylight-saving gaps and overlaps, and why 2038 still matters.

· Mohammed Aquib Ansari

Most of my timestamp bugs haven’t been about math. They’ve been about assumptions: that a number was in seconds when it was milliseconds, that a log was in UTC when it was local time, or that “01:30 on 27 October” was one moment when it was two. This guide covers the checks I run whenever a timestamp crosses a system boundary, using the Unix timestamp converter.

What a Unix timestamp is

Unix time counts seconds since 1970-01-01T00:00:00 UTC, ignoring leap seconds. So:

0            → 1970-01-01T00:00:00Z
1700000000   → 2023-11-14T22:13:20Z
2147483647   → 2038-01-19T03:14:07Z

A timestamp has no time zone. It names one instant everywhere on Earth; the zone only matters when you display it.

Seconds, milliseconds, microseconds, nanoseconds

The same instant appears with different precision depending on who wrote it:

SourceUnitDigits today
date +%s, PostgreSQL EXTRACT(EPOCH …), most C APIsseconds10
JavaScript Date.now(), Java, many JSON APIsmilliseconds13
Python time.time_ns() // 1000, some tracing systemsmicroseconds16
Go UnixNano(), OpenTelemetrynanoseconds19

The converter guesses the unit from the digit count and tells you what it assumed, because the classic mistake is silent: read 1700000000000 as seconds and you get a date in the year 55,841; read 1700000000 as milliseconds and you get January 1970. If the guess is wrong, pick the unit manually.

Precision is kept all the way through. 1700000000123456789 becomes 2023-11-14T22:13:20.123456789Z — the converter works in 64-bit-safe integers rather than floating point, which would quietly round the last digits.

Time zones

A timestamp becomes a wall-clock time only when you pick a zone:

1700000000 in UTC           → 2023-11-14T22:13:20Z
1700000000 in Asia/Kolkata  → 2023-11-15T03:43:20+05:30

Same instant, different date. When a user in India reports that something happened “on the 15th”, and your UTC logs say the 14th, both are right. The converter defaults to your browser’s time zone, always shows UTC alongside, and lets you pick any IANA zone name.

Converting dates back to timestamps

This direction is where real bugs hide, because daylight saving time makes local time non-unique.

Clocks go back (end of summer time), and an hour happens twice. In London on 27 October 2024, 01:30 occurred once at +01:00 and again an hour later at +00:00. Ask most libraries for “2024-10-27 01:30 Europe/London” and you get one of the two without warning. The converter shows both timestamps.

Clocks go forward, and an hour doesn’t exist. In New York, 2024-03-10 02:30 never happened — clocks jumped from 01:59:59 to 03:00:00. The converter reports it as non-existent and shows the time most libraries shift it to (03:30 EDT), so you can decide whether that’s acceptable.

If you control the data format, avoid the problem entirely: store UTC, or store local time with its offset (2024-10-27T01:30:00+01:00). An input with an explicit offset is converted exactly, whatever zone is selected.

Ambiguous date formats

01/02/2026 is 2 January in the US and 1 February in the UK and India. The converter refuses to guess and asks for the date order, unless the numbers make it obvious (like 25/12/2026). ISO 8601 (2026-02-01) is never ambiguous, which is why I use it in anything a machine might read.

The Year 2038 problem

A signed 32-bit integer holds at most 2,147,483,647. As seconds since 1970, that’s 2038-01-19T03:14:07Z. One second later, 32-bit time_t wraps to 1901. Desktop operating systems and databases have mostly moved to 64-bit time, but it’s alive and well in:

  • microcontroller firmware that stores time in int32_t or long on a 32-bit MCU,
  • file formats and network protocols with 32-bit timestamp fields,
  • old database columns (MySQL’s TIMESTAMP type ends at 2038-01-19),
  • certificates and license expiry dates set far in the future.

The converter flags any timestamp past the limit. For expiry dates and long-lived records, that check is worth doing now, not in 2037.

Leap seconds

Unix time pretends every day is exactly 86,400 seconds, so the 27 leap seconds added since 1972 aren’t counted. A time like 2016-12-31T23:59:60Z existed in UTC but can’t be written as a Unix timestamp; the converter rejects second 60 and explains why. For almost all application code this doesn’t matter. For precise interval measurement, use a monotonic clock instead of wall time.

Getting “now” in code

The converter includes one-liners for eight environments. The two I reach for most:

DateTimeOffset.UtcNow.ToUnixTimeSeconds()
date +%s

On an ESP32 or Arduino, remember that millis() counts from boot, not from 1970 — you need NTP (or an RTC) before time() returns a real timestamp.

A quick checklist

  • Count the digits before assuming the unit.
  • Show UTC next to local time in logs and bug reports.
  • Store UTC or an explicit offset; never bare local time.
  • Treat DST transition hours as special in schedulers — pair this with the cron parser to see when jobs actually run.
  • Check anything that will still exist in 2038.

Batch mode in the timestamp converter converts a whole column of timestamps at once and reports errors per line, which is the fastest way I know to sanity-check an export before it goes anywhere else. If the IDs in that export are UUID v7, the UUID generator can decode the timestamp embedded in each one.