Seconds, Milliseconds, 2038, and the Timezone Bugs Hidden in Timestamps
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.
Primary references
Standards and official documentation used to check the technical details in this guide.
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
Continue with practical guides from the same topic area.
What Actually Belongs in a Production Dockerfile for Node (and What Doesn't)
Most Node Dockerfiles in the wild copy node_modules into the image, run as root, and ship a 900 MB layer. A short guide to the few decisions that matter: base image, multi-stage builds, layer caching for dependencies, the NODE_ENV trap, and why your docker-compose should not mirror production.
Catching Missing Translation Keys and Interpolation Mismatches Before Users Do
A missing translation key renders the raw key path to users, and a mismatched interpolation parameter renders an empty string or a crash. Both are easy to miss in review because the developer's locale always has every key. A guide to comparing locale JSON files, finding missing keys, and catching parameter mismatches before they ship.
Mock Data That Actually Exercises Your UI (Not Just Fills It)
Most mock data is ten copies of the same row with a different id. It fills the page and tests nothing. A guide to generating mock data that exercises layout edge cases, long names, missing fields, empty states, and the date and number formats that break formatting code, with field inference so a sample JSON becomes a realistic dataset in one step.