Unix timestamps
What the Unix epoch is, seconds vs milliseconds detection, the 2038 problem, ISO 8601 vs epoch, and timezone handling in JS, Python, and Postgres.
A Unix timestamp is a single number: the count of seconds (or milliseconds) since 1970-01-01T00:00:00Z, the "Unix epoch". It's the closest thing programming has to a universal clock — one integer, no timezone, no ambiguity. Except when there is.
Why 1970?
Ken Thompson picked it because it was recent and round. It stuck. Every mainstream language, database, and OS uses this epoch, which makes timestamps trivially portable — a huge win compared to the alternative (parsing date strings across locales).
Seconds vs milliseconds
Both are common and both are called "Unix timestamps". Rules of thumb:
- Unix / Linux, Postgres, Go, Python
time.time()— seconds (may include fractional). - JavaScript
Date.now(), JavaSystem.currentTimeMillis()— milliseconds. - JWT
iat/exp— always seconds. Every time.
Order of magnitude is the giveaway: 10-digit values ~1.7×10⁹ are seconds (current era). 13-digit values ~1.7×10¹² are milliseconds. The Unix timestamp converter auto-detects using this heuristic (≥10¹² = ms).
Timezones — the honest version
A Unix timestamp is always UTC. There's no such thing as a "Unix timestamp in PST". Timezones only enter the picture when you format the timestamp for humans. Store as a timestamp; display in the user's timezone. Never the other way around.
// JS — display an ISO timestamp in a user's timezone
new Intl.DateTimeFormat("en-US", {
timeZone: "America/Los_Angeles",
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(ts));
// Python
from datetime import datetime, timezone
datetime.fromtimestamp(ts, tz=ZoneInfo("America/Los_Angeles"))
// Postgres — store as TIMESTAMPTZ (which is really UTC), display with AT TIME ZONE
SELECT created_at AT TIME ZONE 'America/Los_Angeles' FROM events;The 2038 problem
A signed 32-bit integer maxes out at 2,147,483,647, which corresponds to 2038-01-19T03:14:07Z. Systems that store timestamps in 32-bit ints will overflow that day. Nearly all 64-bit software has already moved to 64-bit timestamps, but legacy embedded systems, old databases, and file formats can still bite you. If you're picking a column type in 2026, use BIGINT or a native timestamp type, not INT.
Leap seconds
Unix time pretends leap seconds don't exist — a Unix day is exactly 86,400 seconds. When a real leap second is inserted, most systems "smear" it over several hours (Google's approach, now standard on AWS/GCP) so nothing ever repeats or skips. For 99.9% of applications you can ignore leap seconds entirely.
ISO 8601 — the human-friendly alternative
When you need to store or transmit a timestamp in a text format, use ISO 8601 (a.k.a. RFC 3339 for the strict subset):
2026-07-04T14:30:00Z ← UTC, "Zulu" time 2026-07-04T07:30:00-07:00 ← same instant, offset form 2026-07-04T14:30:00.123456789Z ← nanosecond precision (RFC 3339 accepts fractional)
Advantages over Unix epoch: sorts lexicographically, human-readable, includes timezone info explicitly. Every language has a parser. Always include the timezone — a bare 2026-07-04T14:30:00 is ambiguous.
Practical rules
- Store timestamps as UTC. Always.
- Pick one unit per system (seconds or milliseconds) and stick with it.
- Display in the user's timezone at the very last moment, in the UI layer.
- Use
BIGINTor a native timestamp type in databases — notINT. - Prefer
TIMESTAMPTZoverTIMESTAMPin Postgres. Every day. - When logging, use ISO 8601 with a
Zsuffix — future you will thank you.