Skip to content
yutils

How Unix Time Actually Works

Why 1970, why Unix time isn't actually monotonic, the Y2038 problem on 32-bit systems, the leap-second mess, monotonic vs wall clocks, and why "now()" can go backwards.

~8 min read

1716345600 is a timestamp — 1.7 billion seconds since midnight UTC on January 1, 1970. Why 1970? Why isn't Unix time monotonically increasing? Why are people still worried about 2038? This guide walks through Unix time's definition, the leap-second mess, wall clock vs monotonic clock, and the Y2038 problem.

Unix epoch — why 1970-01-01?

Unix's first release shipped in 1971. The system time was a 32-bit signed integer in units of 1/60 second:

32-bit signed × 1/60 sec = ±2^31 / 60 / 60 / 60 / 24 / 365 ≈ ±1.13 years

→ Starting from 1971 would only reach August 1972. Way too short.

By Unix 4 (1973), the unit became 1 second:

32-bit signed × 1 sec = ±2^31 seconds ≈ ±68.1 years

→ With a 1970-01-01 epoch, the representable range is 1902 to 2038.

Picking exactly 1970-01-01 00:00:00 UTC was an arbitrary clean round number — pre-Unix and easy to remember.

Unix time = "seconds since 1970" — but not monotonic

The textbook definition — Unix timestamp = elapsed seconds since 1970-01-01 UTC. In practice:

  • Leap seconds are ignored — Unix time enforces "1 day = 86,400 seconds." The occasional leap second (when Earth's rotation slows down) is cleverly swept under the rug.
  • Can go backwards — NTP sync, user manually setting the clock, DST end.

Leap seconds — 27 inserted since 1972

Earth's rotation gradually slows down, so UT1 (astronomical) and TAI (atomic) drift apart. UTC = TAI + leap seconds. Between 1972 and 2017, 27 leap seconds were added (none since).

How Unix time handles them:

Regular midnight: 23:59:58 → 23:59:59 → 00:00:00
Leap midnight:    23:59:58 → 23:59:59 → 23:59:60 → 00:00:00
                                          ↑ leap second

Unix system behavior:
- Option A (clock stops): 23:59:59 lasts 2 seconds → monotonic OK
  but the clock "freezes"
- Option B (clock smear): smear the leap second over 24 hours →
  Google's choice
- Option C (repeat): 23:59:59.5 → 23:59:60.0 → 00:00:00 → not monotonic

Cloudflare took an outage in 2017 from a leap-second handling bug. The IERS has approved phasing out leap seconds by 2035.

Y2038 — the next big timestamp incident

2147483647 = 2038-01-19 03:14:07 UTC
2147483648 → -2147483648 (32-bit signed overflow)
           = 1901-12-13 20:45:52 UTC

Just before the max value of a 32-bit signed Unix time. The next second wraps around to a negative number — December 1901.

Affected systems:

  • Old IoT devices (firmware can't be updated)
  • Embedded Linux on 32-bit ABIs
  • C code where time_t is still 32-bit
  • MySQL's TIMESTAMP column (4 bytes) caps in 2038 (DATETIME is fine)
  • UNIX file system inodes (ext2/3 mtime/atime fields)

Fix — 64-bit time_t. Modern systems already use it (representable range ~292 billion years), but legacy code and DB schemas remain. Y2K-style audit work needs to happen.

Wall clock vs monotonic clock

The OS exposes two kinds of clock:

Wall clock (system time)

  • The "real" date/time. Adjusted by NTP.
  • User-modifiable (System Preferences).
  • Affected by leap seconds and DST.
  • Can go backwards.

Examples — Node Date.now(), Python time.time(), Java System.currentTimeMillis().

Monotonic clock

  • A counter since OS boot that only increases.
  • Unaffected by NTP or user changes.
  • No "real time" meaning — only durations between samples.
  • Never goes backwards.

Examples — Node performance.now(), Python time.monotonic(), Java System.nanoTime().

Which one to use

  • "What time is it?" / log timestamps / DB rows — wall clock
  • "How long did this take?" / timeouts / perf measurement — monotonic
// Bad — using wall clock for duration
const start = Date.now();
heavyTask();
const elapsed = Date.now() - start;
// If NTP adjusts wall clock by -5s in between, elapsed is wrong (or negative)!

// Good — monotonic
const start = performance.now();
heavyTask();
const elapsed = performance.now() - start;
// Always ≥ 0, unaffected by NTP

Timezone gotchas

Unix timestamps are timezone-agnostic — always UTC under the hood. Timezone matters only at display time:

Unix time 1716345600
  → UTC:   2024-05-22 00:00:00
  → KST:   2024-05-22 09:00:00 (UTC+9)
  → PST:   2024-05-21 17:00:00 (UTC-7, summer)

See the timezone-pitfalls guide for the full landmine field.

DST cutover

Spring forward / fall back. South Korea doesn't observe DST, but US/EU users do:

2024-11-03 (DST ends), US Eastern:
  02:00:00 → 01:00:00 (clock moves back 1 hour)

If a user enters 01:30 Eastern in that window, which 01:30? Ambiguous.

Always store UTC + apply timezone only at display.

Resolution — seconds, ms, μs, ns

UnitCommon useUnix timestamp
SecondsUnix, syslog, cron1716345600 (10 digits)
Milliseconds (ms)JavaScript Date, Java1716345600000 (13 digits)
Microseconds (μs)Python time, Postgres1716345600000000 (16 digits)
Nanoseconds (ns)Go time, Linux kernel1716345600000000000 (19 digits)

13 digits = ms. 10 digits = seconds. Check the length first when unsure.

Unix Timestamp Converter auto-detects any resolution.

ISO 8601 vs Unix timestamp

The two ways to spell a moment:

  • Unix timestamp — a single integer. Compact, fast to compare, no timezone. Hard to read.
  • ISO 8601 2024-05-22T00:00:00Z. Readable, optional timezone. Parser overhead.

In practice — DBs use timestamp/timestamptz (fast integer compare). APIs return ISO 8601 (humans + timezone).

Common pitfalls

1. Date string ambiguity

new Date("2024-05-22")
// → 2024-05-22T00:00:00.000Z (UTC midnight)
// Did the user mean local midnight? Different timestamp.

2. JavaScript's 0-indexed months

new Date(2024, 5, 22)   // ← 2024-06-22 (5 = June, not May!)

3. Guessing the timestamp unit

An API hands you a number — is it seconds or ms? Length tells you: 13 = ms, 10 = seconds. Unix Timestamp Converter auto-detects.

4. setTimeout resolution limits

setTimeout(cb, 1);   // Actually ~4ms minimum
                     // Modern browsers: 1ms (active tab)
                     // Background tab: 1000ms (throttled)

setInterval drifts over time — re-schedule with setTimeout instead

5. "Add one month" ambiguity

2024-01-31 + 1 month = ?
  → 2024-02-31 (doesn't exist) → 2024-02-29 (varies by library) → 2024-03-02

Date Calculator makes the intermediate steps explicit.

References

Summary

  • Unix epoch = 1970-01-01 UTC. Chosen because a 32-bit signed counter only spans ±68 years.
  • Unix time enforces "1 day = 86,400 seconds" and ignores leap seconds. Not strictly monotonic.
  • Y2038 = 32-bit signed time_t overflow on 2038-01-19. Legacy systems still need audits. 64-bit is the modern default.
  • Wall clock (Date.now) vs monotonic clock (performance.now). Always use monotonic for duration measurement.
  • Timezone is a display concern. Store UTC timestamps.
  • Resolution: 10 / 13 / 16 / 19 digits = sec / ms / μs / ns. Length tells you the unit.
  • Try it: Unix Timestamp Converter / Date Formatter / Date Calculator / Time Zone Converter.
Back to guides