Dates & Time
Unix Timestamps — Seconds, Milliseconds, Year 2038, and What to Watch For
Unix timestamps look simple and break in surprising ways. Seconds vs milliseconds, the 2038 problem, and the rules that keep time math from eating your app.
Unix time is the closest thing computing has to a universal clock. It is also the source of some of the most stubborn bugs in data pipelines, because every language, database, and API draws the line in a slightly different place. This guide covers what Unix time is, the three gotchas that cause the bulk of the bugs, and the rules that keep time math clean.
What a Unix timestamp actually is
A Unix timestamp is the number of seconds elapsed since 1970-01-01 00:00:00 UTC, not counting leap seconds. It is a scalar. It carries no timezone because it is always UTC by definition.
0 → 1970-01-01 00:00:00 UTC
1000000000 → 2001-09-09 01:46:40 UTC (the "Billenium")
1700000000 → 2023-11-14 22:13:20 UTC
2147483647 → 2038-01-19 03:14:07 UTC (32-bit signed max)
Because it is a scalar in UTC, two timestamps can be compared with <, subtracted to get a duration, and stored in any integer column without a timezone annotation. That is the whole appeal.
For interactive conversion between timestamps and human-readable dates, the timestamp converter is the quickest way; for the separate problem of rendering a UTC instant in a specific zone, the timezone converter handles that conversion.
Seconds vs milliseconds — the unit problem
Different stacks default to different units. This is the single most common Unix-timestamp bug.
| Unit | Digits (today) | Typical home |
|---|---|---|
| Seconds | 10 | Unix date +%s, Python time.time() (truncated), Postgres epoch, Linux kernel |
| Milliseconds | 13 | JavaScript Date.now(), Java System.currentTimeMillis(), most mobile APIs |
| Microseconds | 16 | Python datetime.timestamp() * 1_000_000, Go time.UnixMicro |
| Nanoseconds | 19 | Go time.UnixNano, Linux clock_gettime(CLOCK_REALTIME) |
A quick detection trick: a timestamp for “now” is around 1.76e9 in seconds. If your value is four orders of magnitude larger, it is in milliseconds. Three more, microseconds. Six more, nanoseconds.
// Detect units from magnitude
function detectUnit(ts) {
if (ts < 1e11) return "seconds"; // ~year 5138
if (ts < 1e14) return "milliseconds"; // ~year 5138
if (ts < 1e17) return "microseconds";
return "nanoseconds";
}
// Normalize anything to milliseconds
function toMillis(ts) {
if (ts < 1e11) return ts * 1000;
if (ts < 1e14) return ts;
if (ts < 1e17) return Math.floor(ts / 1000);
return Math.floor(ts / 1_000_000);
}
Pick one unit per API and document it. If you are building a public API, JSON numbers being integers is not guaranteed past 2^53 - 1, so nanosecond timestamps in JSON should be strings, not numbers.
The Year 2038 problem
A signed 32-bit integer maxes out at 2,147,483,647. Interpreted as seconds since 1970, that is 2038-01-19 03:14:07 UTC. One second later, a naive 32-bit time_t wraps to -2,147,483,648, which is 1901-12-13 20:45:52 UTC.
This was the Y2K-equivalent of the Unix world. On most modern systems it is already fixed:
- 64-bit operating systems have used 64-bit
time_tsince roughly the mid-2000s for Linux userland, 2017 for Windows. - 32-bit Linux systems got 64-bit
time_tin glibc 2.34 (2021) and kernel 5.6+ for new syscalls. - Embedded systems are the remaining risk. Industrial controllers, older routers, some IoT devices with 20-year lifetimes will still fail in 2038.
Two practical consequences today:
- Do not design new systems around 32-bit
time_t. Use 64-bit integers, or milliseconds, or ISO 8601 strings. - If you import data from old systems, check for timestamps near
2147483647. A truncated or wrapped value is the tell.
UTC vs local time — do not be clever
The hard rule: store timestamps as UTC integers. Render them in a user’s local timezone at display time. Never store local time. Never store “local time plus offset” unless you are building a calendar app (and even then, think hard).
The ten-thousand-word version of this rule lives in the timezone handling guide — it is the most frequently violated rule in backend code.
Quick corollary: when you log an event, log its UTC Unix timestamp. Not 2026-04-17 14:33:11 in some local zone, which is ambiguous twice a year during daylight saving transitions.
Leap seconds and why most apps ignore them
Unix time, by explicit definition, does not count leap seconds. A leap second inserted at 23:59:60 UTC on December 31 repeats the same Unix timestamp twice, or (depending on server clock strategy) the clock slews by 1 second over some window around midnight.
For most apps, the correct response is “I do not care”. Durations shorter than a day are unaffected; durations longer than a day have accumulated maybe 27 total leap seconds since 1972 (all positive), which is rounding error for most business logic.
Where it matters:
- Financial systems with regulatory timestamp ordering.
- Physics experiments and astronomy.
- Network protocols that care about sub-second synchronization.
If you have ever seen a log entry labeled 23:59:60, that is a leap second. International Earth Rotation Service announced in 2022 that the last leap second will be inserted no later than 2035.
Ordering, monotonicity, and “now”
Three subtle points:
- System clocks can go backward. NTP slews and jumps. Containers pause and resume. A timestamp you just wrote might be “earlier” than one written five minutes ago. If you need strictly monotonic IDs, use a counter (or a UUID v7 — see the UUID v4 vs v7 piece), not a timestamp.
- “Now” is not a single value. Between
const a = Date.now()andconst b = Date.now(), time has moved. If you are debouncing or rate-limiting, compute “now” once and reuse. - Comparing timestamps from different clocks is unsafe. Distributed systems have no global clock. If the client sends its timestamp and the server has its own, do not subtract them and treat the result as a real duration.
Formatting conventions that survive
When you serialize a timestamp to text, you have two reasonable options:
- An integer in a documented unit (seconds or milliseconds), annotated in the field name:
created_at_ms. - An ISO 8601 string:
2026-04-17T14:33:11Z. TheZ(or+00:00) is required; a bare2026-04-17T14:33:11is a time without a zone and is ambiguous.
Anything else — 14/04/2026, April 17, 2026 2:33 PM, Unix seconds with no unit in the name — is a future bug.
# Unix seconds right now
date +%s
# 1776556391
# ISO 8601 UTC right now
date -u +%Y-%m-%dT%H:%M:%SZ
# 2026-04-17T14:33:11Z
# Convert a Unix timestamp to local ISO
date -d @1776556391
# Fri Apr 17 16:33:11 CEST 2026
Takeaways
Unix timestamps are UTC seconds (or milliseconds, or micros, or nanos — check your stack). Detect the unit by magnitude, normalize at the boundary, document in the field name. The Year 2038 problem is mostly solved on 64-bit systems but real for embedded. Leap seconds are safely ignored by most apps. Never store local time. For interactive conversion, use the timestamp converter; for per-zone display, the timezone converter. For the full zone-handling philosophy, read the backend timezone guide.