# 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.

Source: https://rankcert.com/blog/unix-timestamps-explained
Published: 2026-09-02 · Updated: 2026-09-02

---


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.

```js
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 tool: [Epoch Converter](https://rankcert.com/tools/epoch-converter) - Convert 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](/tools/epoch-converter), which handles both seconds and milliseconds and shows the result in UTC and your local zone.
