Unix Timestamps Explained: Epochs, Time Zones and the Year 2038 Problem

Somewhere in almost every database, log file and API response sits a number like 1783468800 — meaningless to humans, unambiguous to machines. It’s a Unix timestamp: the count of seconds since midnight UTC on January 1, 1970. That one design decision, made half a century ago, still underpins how virtually all software tells time — and it carries a few traps every developer eventually steps in.

Why 1970, and why seconds?

Early Unix needed a compact way to store time. Calendars are a nightmare of irregular months, leap years and local conventions — but a counter is trivial. Pick an arbitrary starting instant (the epoch: 1970-01-01 00:00:00 UTC, chosen simply as a convenient round date near Unix’s birth), then represent any moment as an integer number of seconds since then.

The payoff is huge:

  • Comparison is integer comparison. Which event came first? Smaller number. No parsing “03/04/2025” and wondering if it’s March or April.
  • Arithmetic is subtraction. Duration between two events = one subtraction. “24 hours from now” = add 86,400.
  • No time zones inside the number. A timestamp names a global instant. 1783468800 is the same moment in Tokyo and Toronto; only its rendering into local time differs.

That last property is the underrated one. Time zones, daylight saving, and calendar formats become a display concern, applied at the last moment before a human sees the value. The timestamp converter does exactly that translation in both directions — paste a timestamp, get the human date in your zone and UTC; pick a date, get the integer.

The traps

Seconds vs milliseconds. Unix time is seconds, but JavaScript’s Date.now() returns milliseconds. Mixing them is the classic timestamp bug, and it has a signature: a millisecond value fed into a seconds API produces a date around year 58,000; a second value fed into a millisecond API lands in January 1970. Sanity check by digit count: current time is 10 digits in seconds, 13 in milliseconds. If a date renders as 1970-01-21, you divided when you should have multiplied.

“Local epoch” confusion. The epoch is defined in UTC. A timestamp converted to a date will look “off by a few hours” if you expected local time — it isn’t wrong, it’s UTC. Schedule coordination across offsets is a rendering problem, which is what a time zone converter is for.

Leap seconds. Unix time pretends leap seconds don’t exist — every day is exactly 86,400 seconds. When a leap second occurs, Unix clocks repeat or smear a second. For almost all software this is a feature: durations stay simple, and the sub-second lie is absorbed by clock synchronization.

DST doesn’t exist in timestamps — until you format them. “Run this job at 2:30 AM local time” is genuinely ambiguous twice a year: that wall-clock time occurs twice in autumn and never in spring. Cron schedules, which work in local wall-clock time, inherit this; it’s why once-a-day jobs are safest scheduled outside the 1–3 AM window (a topic the cron expression generator sidesteps by letting you see exactly what fires when).

The Year 2038 problem

Classic Unix systems stored the timestamp in a signed 32-bit integer. The largest value it can hold is 2,147,483,647 — and 2,147,483,647 seconds after the epoch lands at 03:14:07 UTC on January 19, 2038. One second later, the counter wraps to its most negative value, which decodes as December 13, 1901.

If that sounds like Y2K again: it’s structurally worse in one way (the failure is at the binary storage level, not in string formatting) and better in another (the fix has been known for decades). Modern 64-bit systems store time in 64-bit integers, which postpones the overflow by roughly 292 billion years — comfortably after the heat death of the sun becomes a scheduling concern.

The realistic 2038 risk isn’t your laptop. It’s the long tail: embedded controllers, routers, industrial systems, old file formats and database columns (INT instead of BIGINT) with 32-bit time baked in, deployed today with 12+ year lifespans. If you’re designing anything with a signed 32-bit seconds field in 2026, you are shipping a known time bomb.

There’s a smaller cousin already behind us: some systems that stored milliseconds in 64 bits are fine, but GPS’s 10-bit week counter rolls over every 19.6 years (most recently in 2019, next in 2038 — a coincidence of calendars), and it caused real device failures each time. Rollover bugs are a genre, not a one-off.

Practical habits

  1. Store UTC timestamps, render local. The database keeps integers (or UTC datetimes); the user’s browser applies the zone at display time. Never store “local time” without its zone — it’s an incomplete fact.
  2. Name your units. created_at_ms beats created_at forever. Half the timestamp bugs in the wild are unit confusion.
  3. Use 64-bit (or BIGINT) time fields. The storage cost is four bytes; the alternative is a 2038 migration.
  4. Do calendar math in calendar space. “30 days” is 2,592,000 seconds, but “1 month later” is not — months vary, DST shifts wall clocks. Count actual calendar days with a date difference calculator rather than dividing seconds and hoping.
  5. Test the boundaries. Feed your system 0 (the epoch), negative values (pre-1970 dates are valid!), 2,147,483,647, and a 13-digit millisecond value. Each one finds a different class of bug.

A Unix timestamp is the rare piece of computing infrastructure that’s both trivially simple — it’s literally just a counter — and deep enough to carry fifty years of edge cases. Treat the integer as the truth and every human-readable date as a view of it, and most of time’s complexity stays where it belongs: at the display layer. And when you just need to know what 1783468800 means in your own time zone, that’s a ten-second lookup.