Unix Timestamps, Properly Explained
The Unix epoch is 1970-01-01 00:00:00 UTC. There is nothing astronomically, culturally, or numerically significant about that date. The original Unix designers picked the start of the year their system shipped, because that was easier than picking anything else. Fifty-five years later, that arbitrary choice is the universal coordinate origin for digital time.
What it actually is
A Unix timestamp is a count of seconds since the epoch, ignoring leap seconds. That last part matters more than people expect — we'll come back to it.
The number itself has no inherent unit. It's an integer. Whether that integer represents seconds, milliseconds, microseconds, or nanoseconds is a convention you have to know out-of-band. There is no metadata, no marker, no schema. The convention varies by language and ecosystem:
- Seconds (10 digits today): C
time_t, Gotime.Time.Unix(), PHPtime(), Pythontime.time()(returns float seconds), most UNIX command-line tools. - Milliseconds (13 digits today): JavaScript
Date.now(), JavaSystem.currentTimeMillis(), Kotlin / Android, most JSON APIs that emit numbers. - Microseconds (16 digits): PostgreSQL
epochfortimestamptz, some C++ APIs, profiling output. - Nanoseconds (19 digits): Go
time.Time.UnixNano(), recent kernel APIs.
The two-cents trick: a 10-digit timestamp lands somewhere between 2001 and 2286. A 13-digit one is in milliseconds. Anything longer is microseconds or nanoseconds. If a parsing layer treats one as the other, you get values 1,000× off — usually showing up as dates in the year 50000, or in 1970.
How to read a stranger's timestamp
If you're handed an opaque integer:
- Count digits. 10 → seconds, 13 → ms, 16 → μs, 19 → ns. (Until 2286 this rule holds.)
- Convert to UTC and look at the year. If it's wildly wrong, you got the unit wrong.
- If it's a float, the integer part is seconds and the fractional part is sub-second precision. Don't round; preserve.
Don't trust the variable name. You will see timestamp_ms holding seconds, created_at_seconds holding milliseconds, and unix_time holding either.
Y2K38
time_t was historically a signed 32-bit integer on Unix. Signed 32-bit max is 2_147_483_647, which corresponds to 2038-01-19 03:14:07 UTC. At 03:14:08, time_t overflows to negative and points back to 1901.
Almost every desktop and server system has moved to 64-bit time_t. The systems that haven't are exactly the ones nobody wants to touch — embedded firmware in industrial equipment, ATMs, sensors, point-of-sale terminals. Y2K38 will not be an apocalypse on the scale of Y2K, but it will produce a steady drip of mysterious failures starting around 2035 as software written today begins to look 13 years ahead.
If you're storing timestamps for the long haul, use 64-bit integers or int64. If you're using a database column type, prefer BIGINT over INT for any field that might hold a millisecond timestamp — INT overflows in 1970 + (2³¹ − 1)ms ≈ year 1995 already.
Leap seconds: the lie
Unix time pretends every day has exactly 86,400 seconds. The Earth disagrees by about 1 second per 18 months. The international time authorities periodically insert a "leap second" — 23:59:60 UTC — to keep atomic time aligned with mean solar time.
Unix time can't represent 23:59:60. The convention is that the leap second either gets skipped (the Unix clock pauses) or replays the previous second (the clock smears). Most NTP-synchronized servers smear: spread the leap second across 24 hours, so each second is imperceptibly longer. Google publicized its smearing algorithm in 2008; it's now a de-facto standard.
This matters in three places:
- Financial systems that need millisecond-level ordering during the leap second window.
- Anything storing intervals. A 10-second interval across a leap second is 11 actual seconds.
- Logs and metrics — duplicate timestamps on the second boundary, or apparent gaps.
The IERS announced in 2022 that leap seconds are scheduled to be abandoned by 2035. They will still happen, infrequently, until then.
Timezones are not part of the timestamp
This trips up everyone. A Unix timestamp has no timezone. It's a count of seconds since a UTC moment. The timezone enters the picture only when you convert to a wall-clock representation like "2026-06-05 14:30."
What this means in practice:
- Storing a wall-clock string like
"2026-06-05 14:30"without a timezone is data corruption. You don't know what moment it refers to. - Storing a Unix timestamp loses no information about when, only about where the user was when it happened. If your business cares where, store the timezone separately as IANA name (
Asia/Shanghai,America/New_York), not as an offset (+08:00) — offsets don't capture DST rules. - ISO 8601 strings with a
Zor offset suffix (2026-06-05T06:30:00Z) are unambiguous. Without the suffix, they're ambiguous and you should reject them.
DST: the recurring bug
Daylight Saving Time means certain wall-clock times don't exist (the spring-forward gap) and others happen twice (the fall-back overlap).
In US Eastern time, on the second Sunday of March, 2:00 to 3:00 doesn't exist — the clock jumps from 1:59:59 to 3:00:00. A meeting scheduled for "2:30 every Tuesday" in someone's calendar — what does it mean on that Sunday? (It usually means 3:30, because most calendar systems quietly skip the missing hour.)
On the first Sunday of November, 1:00 to 2:00 happens twice. A wall-clock value of 01:30 on that day is ambiguous — it refers to two different Unix timestamps, exactly 3,600 seconds apart.
Tools that get this wrong run cron jobs zero times (in the gap) or twice (in the overlap). Don't store wall-clock + offset pairs across DST transitions. Store the timestamp.
Monotonic time vs wall-clock time
System time can move backwards. The clock can be manually changed. NTP can step the clock forward or back to correct drift. A laptop sleeping for an hour wakes up with a different idea of "now."
If you're measuring an interval — request latency, time since a button was pressed, animation frame timing — use monotonic time, not wall-clock. Monotonic clocks count seconds from an arbitrary system-defined origin (often boot) and never go backwards. The names vary: clock_gettime(CLOCK_MONOTONIC) in C, process.hrtime() in Node, time.monotonic() in Python, Instant in Rust.
If you measure with Date.now() and the system NTP-corrects mid-measurement, your interval can be negative. Worse, it can be very large positive when a manual clock change moved the clock forward. Bugs filed about this are universally "the clock did something weird"; the answer is universally "use monotonic."
Common pitfalls
- Conflating seconds and milliseconds. The 1000× error is the most common timestamp bug in the world.
- Storing local time without timezone. Eventually someone will read your data from a different machine.
- Using offset strings (
+08:00) for storage. They don't capture DST rules; the city's offset on June 1st may not be the same as on December 1st. - Measuring intervals with wall-clock time. Use monotonic.
- Rounding. A timestamp rounded to seconds and back is not the original — you lose up to 999ms of information. Keep precision until display.
- Comparing timestamps from different precision sources without normalizing.
Date.now() > some_seconds_fieldis always true.
Practical rules
- Store UTC. Convert to local time only at the display edge.
- Use 64-bit integers for new code.
BIGINTfor the database. - Pick one precision per system (seconds or milliseconds) and document it. Don't mix.
- For human-perceived intervals, monotonic clock. For points in absolute time, wall clock.
- Store timezones as IANA names, not offsets, when "where" matters.
- Reject wall-clock strings that don't carry timezone information.
Convert and inspect
The timestamp tool on this site converts seconds, milliseconds, and microseconds against any timezone, and flags ambiguous cases (DST repeats, second/millisecond confusion). All client-side.
Open the timestamp toolRelated guides
Keep the session useful with adjacent reading instead of exiting after one article.
QR Codes, Properly Explained
How QR codes actually work — finder patterns, Reed-Solomon error correction, static vs. dynamic redirects, and the real reasons codes fail in print.
Base64, Properly Explained
A 1989 hack for smuggling binary through 7-bit email transports — and why we still use it for JWTs, data URIs, and a hundred other places. Two alphabets, one common decode failure, and the things it categorically isn't.
URL Encoding, Properly Explained
Why %20 and + both mean space, why encodeURI and encodeURIComponent are not interchangeable, and how the HTML form spec quietly invented its own incompatible variant. RFC 3986 vs application/x-www-form-urlencoded.