Unix Timestamp Guide: UTC, Milliseconds and the 2038 Problem
A scheduled job fires at 03:00 UTC. Your monitoring says 03:00, the logs say 11:00, and the customer who reported it saw it happen a full day off. All three numbers are timestamps, and they disagree because each was recorded under a different assumption about what a timestamp is.
Time is the one data type whose representation is never obvious. A number in a column, a string in a JSON payload and a formatted value in a log line can denote the same instant while sorting and comparing completely differently. Getting it wrong does not throw an exception; it silently produces a plausible wrong answer.
The problem: a timestamp is a number with an unwritten contract
A Unix timestamp is the number of seconds elapsed since 1970-01-01T00:00:00Z, counted in UTC, ignoring leap seconds. That is the whole definition. The number carries no zone, so the same instant becomes a different string depending on who formats it: 1759276800 is 2026-10-05T08:00:00Z in UTC and 2026-10-05T16:00:00+08:00 at UTC+8. Both are correct. This is where most "the timestamp is wrong" reports come from — the value was never wrong, the formatting assumption was.
Storing a formatted string instead of an instant compounds it. A column holding 2026-10-05 08:00:00 with no zone is ambiguous by construction, and its meaning changes if the server timezone or daylight saving rules change.
Trap one: seconds versus milliseconds
JavaScript's Date.now() returns milliseconds; almost every Unix timestamp on the wire, in SQL and in shell scripts is in seconds. The mistake does not produce a small error — it produces a date roughly 31,000 years away.
It is easy to miss because digit count is the only clue: 10 digits is seconds, 13 is milliseconds. The dangerous direction is the reverse — milliseconds truncated to seconds lands in 1970, which occasionally looks plausible enough to pass a casual check and then fails a month later when a "recent" record sorts as ancient.
The related trap is unit ambiguity inside the payload. Nothing in 1759276800 says whether it is seconds, milliseconds or microseconds. Some systems (Java, Go, Kafka, S3 metadata) use milliseconds; others use seconds. Record the unit alongside the value, in the field name if you can.
Trap two: the 2038 problem
Unix timestamps are seconds, and a 32-bit signed integer tops out at 2,147,483,647 — which from the epoch is 2038-01-19T03:14:07Z. One second later it wraps to a negative number.
This is not distant theory; it is a live constraint for anything still storing timestamps in 32 bits:
- C and C++ with
time_ton a 32-bit build —time_tis 32-bit unless the platform makes it 64-bit. - MySQL
TIMESTAMPcolumns — the documented range ends at2038-01-19. UseDATETIME, or widen where the platform supports it. - Some embedded and network protocols — 32-bit fields in binary formats and RPC definitions.
Overflow is usually silent: in C it is undefined behaviour, and in many languages it wraps to a negative number. A wrapped timestamp sorts before every valid one, quietly corrupting every "latest first" query. Choosing 64-bit costs nothing today — on 64-bit systems time_t is already 8 bytes, and storing milliseconds in a 64-bit column removes the limit and buys precision.
Trap three: local time where it should never appear
Three places cause real incidents with local time. Cron and schedulers fire on the machine's local time, not UTC — a job defined as 0 3 * * * runs at 3am wherever the server is, and container images often ship UTC while bare metal does not. Log lines should stay UTC precisely because logs from many machines must be comparable. Daylight saving transitions break local arithmetic outright: on two days a year a local time may not exist or may occur twice.
The solution: store an instant, format for the reader
The rule that resolves all three traps: store an unambiguous instant; convert only at the edges. Pick one representation and use it everywhere:
| Representation | Form | Why |
|---|---|---|
| Unix seconds | 1759276800 | Compact, sorts naturally, no parsing ambiguity |
| UTC ISO 8601 | 2026-10-05T08:00:00Z | Self-describing, readable in logs |
What to avoid is a zone-less datetime string — that is not a timestamp, it is a timestamp plus a hidden dependency on a timezone you never recorded. If you must store a local wall-clock time because the business genuinely means "9am in the store's city", store the local time and the IANA zone name beside it.
Two habits close the rest. Keep one unit across a system — if the API speaks milliseconds, the database speaks milliseconds — and convert at the boundary rather than inside business logic. And compare instants: a local string compared lexicographically against a UTC ISO string sorts correctly only when the formats happen to align, which is the coincidence that breaks at the first daylight saving boundary.
Tool walkthrough: reading a timestamp from a real system
The Unix Timestamp Converter converts both ways and — the part that matters for debugging — shows the same instant in UTC and your local zone side by side. Paste a value and you get the formatted date with an explicit offset, the same date in local time, and the value in both seconds and milliseconds.
That last item settles the unit question without arithmetic. Take a timestamp from a log line and look at the resulting year: 1970 means milliseconds read as seconds; a year in the tens of thousands means the opposite. You can also check in reverse — convert a timestamp you know the answer for (a JWT's exp claim, a pod's creationTimestamp) and confirm the tool agrees with your expectation. If both directions agree, the timestamp was fine and the bug is in the formatting or comparison downstream.
Two related readings finish most diagnoses. When a timestamp is correct but the displayed value is wrong, the string was formatted somewhere with an assumed zone. And when two systems disagree by a fixed number of hours, that offset is usually a dropped Z: 2026-10-05T08:00:00 versus 2026-10-05T08:00:00Z differ by exactly your UTC offset.
FAQ
Is a Unix timestamp in seconds or milliseconds? Both exist and the number does not say. Ten digits is seconds, thirteen is milliseconds. Seconds are the Unix convention and what date +%s and SQL UNIX_TIMESTAMP() return; milliseconds are what Date.now() and System.currentTimeMillis() return. Convert explicitly rather than inferring from length.
Why does my converted time differ from the log by several hours? One side formatted in UTC and the other in a local zone — the timestamp is almost certainly correct, the Z suffix is not. A fixed whole-hour difference is a timezone offset; a value landing in 1970 or the far future is a seconds/milliseconds mix-up instead.
What exactly happens in 2038, and does it affect me? A 32-bit signed integer counting seconds from 1970 stops at 2038-01-19T03:14:07Z; one second later it overflows, usually to a negative number that sorts before every valid timestamp. It affects 32-bit time_t builds, MySQL TIMESTAMP columns and 32-bit fields in binary protocols. Storing 64-bit values removes the limit and is cheap to adopt now.
More developer tools on DigDevBox
- Unix Timestamp Converter — convert between epochs, UTC and local time in both directions
- JWT Decoder & Parser — read the
expandiatclaims out of a token - Cron Expression Generator — validate a schedule and preview its next run times
- HTTP Status Checker — confirm a live endpoint's actual response