Unix timestamps, epochs, and the year 2038 problem
What a Unix timestamp actually is, the seconds-vs-milliseconds bug that bites everyone, and why signed 32-bit time overflows at 03:14:07 UTC on 19 January 2038.
英語版を表示しています。翻訳は準備中です。
A Unix timestamp is a single integer: the number of seconds elapsed
since 1970-01-01 00:00:00 UTC, the moment known as the epoch. That
one design decision — count seconds from a fixed point in UTC —
makes timestamps cheap to store, trivial to compare, and free of
timezone ambiguity. It also sets up two of the most common bugs in
production systems: the off-by-1000 error between seconds and
milliseconds, and the overflow that will hit signed 32-bit clocks at
a precise instant in 2038.
What the number actually represents
The value is a monotonic count of seconds. 0 is the epoch.
1000000000 is 2001-09-09 01:46:40 UTC. 1700000000 is
2023-11-14 22:13:20 UTC. In mid-2026, a current timestamp in seconds
is a ten-digit number starting with 17.
The count is timezone-independent at storage time. A timestamp carries no offset, locale, or "this was Tokyo time" flag. It is an instant on the global timeline, expressed as seconds-from-epoch in UTC, so two machines on opposite sides of the planet produce the same timestamp for the same instant. The timezone only enters when you convert that integer into a human-readable string — a presentation concern, not a storage one.
That is what makes timestamps good for storage: exactly one canonical value per instant, with no conversion needed to sort, diff, or compare events from different sources.
Seconds vs milliseconds vs microseconds
The single most frequent timestamp bug has nothing to do with 2038. It is unit confusion. Different platforms use different resolutions:
- Seconds — most Unix system calls, POSIX
time(), the Linuxdate +%scommand, PostgreSQLEXTRACT(EPOCH …), most database epoch columns, JWTexp/iat/nbfclaims. - Milliseconds — JavaScript.
Date.now()returns milliseconds, not seconds. So do Java'sSystem.currentTimeMillis()and most JVM date APIs. - Microseconds / nanoseconds — high-resolution timers, some
tracing systems, Go's
time.UnixNano().
The classic failure is passing a JavaScript millisecond value into a
function expecting seconds, or vice versa — an off-by-1000 that lands
you in the wrong millennium. A seconds value of 1700000000 read as
milliseconds is 1970-01-20, twenty days after the epoch. A
milliseconds value of 1700000000000 read as seconds is the year
55840.
You can usually tell which unit you have by digit count, for any time in the current era:
- 10 digits → seconds (e.g.
1748000000, May 2025) - 13 digits → milliseconds (e.g.
1748000000000) - 16 digits → microseconds
- 19 digits → nanoseconds
This heuristic holds from 2001 (10-digit seconds begin) until 2286 (seconds reach 11 digits) — so for any timestamp you realistically handle, digit count tells you the unit. When in doubt, convert both interpretations and keep the one that lands in a plausible decade.
The year 2038 problem
Many older systems store time in a signed 32-bit integer — the C
time_t type as it was historically defined on 32-bit platforms. A
signed 32-bit integer can hold values from −2147483648 to
2147483647. That upper bound, 2^31 − 1 = 2147483647 seconds after
the epoch, is:
2038-01-19 03:14:07 UTC
At exactly 03:14:08 UTC on 19 January 2038, the counter cannot
increment without overflowing. In a signed integer, incrementing past
2147483647 wraps to −2147483648, which is 1901-12-13 20:45:52 UTC. A clock that does this jumps from January 2038 to December
1901. This is the Year 2038 problem, sometimes written Y2038 or the
"Epochalypse."
What actually breaks:
- Embedded and legacy systems — routers, industrial controllers, point-of-sale terminals, and other long-lived devices running 32-bit firmware that will not be patched.
- Old database schemas — columns or serialization formats that
pinned time to 32-bit. MySQL's
TIMESTAMPtype historically topped out at2038-01-19 03:14:07 UTCfor this reason;DATETIMEdid not have the limit. - Software that computes future dates — a 30-year mortgage
amortization, a long-expiry certificate, a far-future cache TTL.
These hit 2038 arithmetic today, not in 2038. Code computing dates
past January 2038 has been failing on 32-bit
time_tfor years.
The fix is a 64-bit time_t. A signed 64-bit integer counts seconds
to about the year 292277026596 — roughly 292 billion years out,
which retires the problem permanently. 64-bit operating systems and
modern languages already use 64-bit time, and Linux extended time_t
on 32-bit architectures in the kernel and glibc around 2020–2021. The
migration reality is that the risk now lives in deployed firmware and
old binaries, not in current compilers: anything you compile fresh on
a 64-bit platform is already safe; the danger is the 32-bit device in
the field that nobody will recompile.
The 1901 lower bound and unsigned variants
The same 32-bit width creates a floor. A signed 32-bit time_t
cannot represent any instant before 1901-12-13 20:45:52 UTC (the
−2147483648 boundary). Dates earlier than that — a 19th-century
birth date, a historical event — overflow downward. This is why some
older systems mishandle pre-1901 dates entirely.
A few systems used an unsigned 32-bit integer instead. It cannot
represent times before the epoch (no negative values) but pushes the
upper bound to 2^32 − 1 = 4294967295, or 2106-02-07 06:28:15 UTC —
buying ~68 more years at the cost of all pre-1970 timestamps. You
still see it in some network protocols and file formats, but it is a
workaround, not a general solution.
Timezones live in the display layer
Because a timestamp is UTC by definition, the timezone is never part
of the stored value. It belongs entirely to the layer that formats
the integer for a human. The same 1748000000 renders as a different
wall-clock string in Tokyo, London, and New York, but it is the same
instant and the same stored number.
The most common timezone bug is the inverse mistake: taking a local
wall-clock time and storing its components as if they were already a
UTC timestamp. If a server in America/New_York reads "3:00 PM"
local and writes the seconds-from-epoch for "3:00 PM UTC," every
consumer is off by the offset — five hours in winter, four in summer,
shifting with daylight saving. The rule: convert to UTC the moment you
capture an instant, store the timestamp, and apply a timezone only
when rendering for a person.
Leap seconds: Unix time pretends they don't exist
UTC occasionally inserts a leap second to keep clocks aligned with
the Earth's rotation; 27 have been added since 1972. Unix time
ignores them. By definition, a Unix timestamp assumes every day is
exactly 86400 seconds long. When a leap second occurs, Unix time
does not get a new value — the clock typically repeats or smears a
second so the count stays consistent with the 86400-per-day model.
The practical implications:
- A Unix timestamp is not a true count of physical SI seconds since 1970. It is off by the number of leap seconds inserted.
- Computing an exact duration between two timestamps spanning a leap second will be off by a second versus a real stopwatch.
- During a leap second, two distinct real instants can map to the same Unix timestamp, so timestamps are not guaranteed strictly unique at sub-second boundaries.
For almost all application code this does not matter. For high- precision timing, trade sequencing, or scientific work it does, and you reach for TAI or a leap-smearing clock source instead.
ISO 8601 vs. raw timestamps
A Unix timestamp is ideal for machines and useless to humans —
1748000000 tells you nothing at a glance. ISO 8601 is the
human-readable interchange format: 2025-05-23T11:33:20Z, where the
trailing Z means UTC ("Zulu"), or with an explicit offset like
2025-05-23T07:33:20-04:00.
When to use which:
- Store a Unix timestamp when the value is internal and you want the smallest, fastest representation for arithmetic or comparison.
- Store or transmit ISO 8601 when humans or heterogeneous systems read the value, when you need an explicit offset, or when the field lands in a log, an API payload, or a config file someone will eyeball. As a bonus, ISO 8601 sorts lexicographically in UTC.
A reasonable default: store timestamps internally, serialize ISO 8601 at API boundaries.
Converting between them
When you are staring at a ten- or thirteen-digit number in a log line and need to know the instant — or need to produce a timestamp for a given date — our Timestamp Converter handles both directions, auto-detects seconds versus milliseconds, and shows the result in UTC and your local timezone side by side. It is the fastest way to settle an "off by 1000 or off by a timezone" question without opening a REPL.