Unix timestamps: seconds, timezones and 2038

2026-09-02·Developer reference·3 min read·by Sourabh Singh

Unix timestamps: seconds, timezones and 2038

What a Unix timestamp counts, how to tell seconds from milliseconds, why leap seconds are ignored, and how to store dates without a timezone bug.

Unix timestamps: seconds, timezones and 2038

A Unix timestamp counts seconds elapsed since 1 January 1970 UTC, ignoring leap seconds. Ten digits means seconds and thirteen means milliseconds - the classic bug is reading one as the other, which lands you in 1970 or in the year 55000. Timestamps are always UTC; the date you see depends on the zone used to format them.

Unix time is a single integer counting seconds since midnight UTC on 1 January 1970. Its value is that it identifies an instant with no timezone, no calendar and no locale attached - all the complexity lives in formatting, not storage.

Seconds or milliseconds

Count the digits.

  • 10 digits - seconds. 1788393600 is September 2026.
  • 13 digits - milliseconds. JavaScript's Date.now() returns these.

Reading a seconds value as milliseconds gives a date in January 1970. Reading milliseconds as seconds gives a date fifty thousand years out. Both are obvious once you see the result, which is the only reason this bug ever gets caught.

Math.floor(Date.now() / 1000)   // seconds
new Date(1788393600 * 1000)     // seconds -> Date

Python's time.time() returns a float in seconds. Java's System.currentTimeMillis() returns milliseconds. Postgres extract(epoch from now()) returns seconds with a fractional part. Mixing two languages in one pipeline is where this bites.

Free toolEpoch ConverterConvert Unix timestamps to human-readable dates and back, handling both seconds and milliseconds, with UTC, ISO 8601 and local output.

Leap seconds are ignored

Unix time assumes every day is exactly 86,400 seconds. Leap seconds - inserted occasionally to keep clocks aligned with the Earth's rotation - are absorbed by repeating or smearing a second rather than incrementing the counter.

This means Unix time is not a true count of elapsed SI seconds since 1970. It is off by the number of leap seconds since then. For everything except satellite navigation and high-precision physics, this is a feature: arithmetic on timestamps stays simple because every day is the same length.

Google and AWS "smear" the leap second across a day rather than repeating it, which keeps monotonic clocks monotonic.

The 2038 problem

A signed 32-bit integer overflows at 03:14:07 UTC on 19 January 2038. Systems storing timestamps in int32 will wrap to 1901.

Most modern systems use 64-bit time and are fine for the next 292 billion years. The remaining risk is in embedded devices, old database columns typed as int, file formats with fixed-width fields, and any protocol that serialises a timestamp into four bytes. If you are choosing a column type today, use bigint or a native timestamp type, never int.

Timezones

A Unix timestamp has no timezone. It is an instant. The timezone appears only when you format it for a human.

This is why a timestamp near midnight shows a different date depending on where you look at it. 1788393599 is late on 1 September in London and early evening on 1 September in New York, but a timestamp two seconds later crosses the date boundary in one and not the other.

The practical consequence: never derive a calendar date from a timestamp without specifying the zone you want it in. new Date(ts).toISOString().slice(0, 10) gives the UTC date, which is often not the date the user experienced.

What to store

An instant - when something happened. Store as timestamptz in Postgres, which stores UTC and converts on the way in and out. A Unix integer works too and is unambiguous.

A calendar date - a birthday, an invoice date, a public holiday. Store as a date. These are not instants and converting them to a timestamp introduces a timezone that does not belong to them. A birthday is the same day everywhere; a timestamp for it is not.

A future local time - a meeting at 9am in Berlin next March. Store the local time and the zone name separately, not a UTC instant. Timezone rules change, and when they do, the instant you computed is wrong while the local intent is still right.

Reading a timestamp quickly

date -r 1788393600 on macOS, date -d @1788393600 on GNU. In SQL, to_timestamp(1788393600). Or paste it into the epoch converter, which handles both seconds and milliseconds and shows the result in UTC and your local zone.

Tools from this guide