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.
1788393600is 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.
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.
