Unix Timestamp Conversion: Seconds, Milliseconds and 2038
A timestamp is just a number, and the number alone does not tell you its unit. Use the wrong unit and a 2026 date renders as 1970 - or as the year 50,000. Getting digit count, timezone and precision straight prevents nearly all of these bugs.
- The digit count defines the unit - 10 digits is seconds, 13 is milliseconds, 16 is microseconds and 19 is nanoseconds, with each step a factor of 1000
- A timestamp is always UTC and carries no timezone; timezones only matter when a timestamp is rendered as a human-readable date
- Parsing a 10-digit value as milliseconds yields a date near 1970-01-01, while parsing a 13-digit value as seconds lands tens of thousands of years in the future
- A signed 32-bit integer overflows at 2147483647, which is 2038-01-19 03:14:07 UTC - the Year 2038 problem
- Store timestamps in UTC and convert only at display time to avoid off-by-one-hour bugs around DST transitions
#The digit count tells you the unit
A Unix timestamp is an offset from 1970-01-01 00:00:00 UTC, and its length tells you the unit. Keep this table handy; it resolves most "the date is wrong" reports on sight.
| Digits | Unit | Typical source | Valid until |
|---|---|---|---|
| 10 | seconds | PHP time(), Linux date +%s | Year 2286 |
| 13 | milliseconds | JavaScript Date.now(), Java System.currentTimeMillis() | ~280,000 years |
| 16 | microseconds | Python time.time_ns() // 1000 | - |
| 19 | nanoseconds | Go time.Now().UnixNano() | - |
The rule is simple: count the digits, then decide whether to divide by 1000. Adjacent magnitudes always differ by exactly 1000, which is why a unit mistake throws the date off by a factor of a thousand.
These two failures are the easiest to diagnose. Parsing a 13-digit value as seconds produces a date around 1970-01-01. Parsing a 10-digit value as milliseconds produces a date tens of thousands of years ahead. If your date lands in January 1970, milliseconds were almost certainly read as seconds.
#Why timezones are irrelevant to a timestamp
A timestamp identifies an absolute instant, and that instant does not change based on where the observer stands. Noon in Beijing on 2026-10-14 and 04:00 UTC on the same day are the same moment, so they share one timestamp.
A timezone only enters the picture at the last step, when that instant is rendered as a readable date string:
| Timestamp (s) | UTC | Beijing (UTC+8) |
|---|---|---|
| 0 | 1970-01-01 00:00 | 1970-01-01 08:00 |
| 1791950400 | 2026-10-14 04:00 | 2026-10-14 12:00 |
So the correct pattern is: store UTC timestamps everywhere in the database and API, and convert to the viewer's timezone only for display. Store local time as a string and cross-timezone debugging plus DST transitions will keep producing bugs.
China does not observe daylight saving time, so teams serving only domestic users rarely hit this. If your service serves Europe or North America, expect a one-hour drift on DST switchover days - the usual root cause is storing a local-time string where a timestamp belongs.
#Three conversion scenarios
#Scenario 1: date to timestamp
Defaults differ per language and database, which is where cross-team bugs originate:
| Environment | Unit returned | Note |
|---|---|---|
| JavaScript | milliseconds | Date.now() and new Date().getTime() are 13 digits |
| PHP | seconds | time() and strtotime() are 10 digits |
| Python | seconds (float) | time.time() includes fractional part; use //1000 for ms |
| Go | seconds | time.Now().Unix() for seconds, UnixMilli() for ms |
| MySQL | seconds | UNIX_TIMESTAMP(); divide by 1000 to handle milliseconds |
When values cross a language boundary, put the unit in the field name. created_at_ms prevents every future reader from having to guess.
If you also need a unique identifier per record, consider a time-ordered scheme - the v7 variant from UUID / GUID Generator increases with time, satisfying uniqueness and sortability at once.
#Scenario 2: timestamp to date
Paste the value into the Unix Timestamp tool - it infers the unit from the digit count and lets you override. On the command line:
# seconds to local time
date -d @1791950400
# milliseconds to seconds to local time
date -d @$((1791950400000/1000))#Scenario 3: measuring an interval
Subtract values in the same unit. Durations are stated in seconds or milliseconds, and mixing them produces a result that is off by 1000x:
{ "created_at_ms": 1791950400000, "ttl_sec": 3600 }Field names like these eliminate most misuse, because the unit travels with the value.
To confirm two datasets migrated consistently, put both timestamp columns side by side with Text Diff. And when inspecting a timestamp field in an API response, run it through JSON Formatter first so the field names are visible at a glance.
#The Year 2038 problem: when it still matters
The largest value a signed 32-bit integer holds is 2147483647, which corresponds to 2038-01-19 03:14:07 UTC. Past that instant, programs storing seconds in 32 bits overflow into negative numbers and report dates around 1901.
Do you still need to care? It depends on the environment:
| Environment | Affected | Why |
|---|---|---|
| 64-bit Linux / macOS / Windows | No | 64-bit timestamps last for billions of years |
| Modern runtimes (Go, Rust, Node, Java 8+) | No | 64-bit internally |
| Embedded devices, legacy C, 32-bit systems | Yes | Still running in production everywhere |
Database columns declared INT instead of BIGINT | Yes | Common in schemas designed a decade ago |
Declare timestamp columns as BIGINT and store milliseconds in any new schema. The cost is four extra bytes per row, and the benefit is never having to migrate before 2038.
#Frequently asked questions
#What date is timestamp 0?
Timestamp 0 is 1970-01-01 00:00:00 UTC, which is 1970-01-01 08:00 in Beijing. It frequently serves as an "unset" default, so a table full of 1970 dates usually means the column was never written rather than that the event happened then. Negative values are legal and represent instants before 1970.
#Why is the time in my frontend off by 8 hours from the backend?
Two likely causes. Either the backend returned a local-time string and the frontend parsed it as UTC, or the backend treated local time as a timestamp. Diagnosis: divide the returned value by 3600 - if the difference is exactly the local UTC offset, the bug is in timezone handling, not in the timestamp format.
#Can I truncate a millisecond timestamp to seconds?
Yes, but understand the cost: truncation reduces precision to whole seconds, so records created within the same second can no longer be ordered. That is fine for counting how many events happened on a given day. Keep milliseconds if you sort by the value.
#Do timestamps account for leap seconds?
No. Unix time flattens leap seconds by defining a day as exactly 86400 seconds, so the system clock repeats or skips an instant when a leap second occurs. The resulting drift against UTC is negligible for almost all software; only astronomy and satellite systems handle it explicitly.
#How can I sanity-check a conversion?
Use two known anchors: 0 must render as 1970-01-01 00:00 UTC, and 1000000000 must render as 2001-09-09 01:46:40 UTC. If both are correct, the tool and timezone settings are fine and you can inspect the specific value. To reason about recurring intervals instead, use Cron Generator.
Tools used in this guide
Convert Unix timestamps to dates and back — 10-digit seconds or 13-digit mill…
Open tool →Cron GeneratorVisual cron expression generator and parser
Open tool →Text DiffSide-by-side line diff with add/delete highlight
Open tool →UUID / GUID GeneratorGenerate UUID v4 random, v1 timestamp, v7 sortable
Open tool →JSON FormatterFormat, validate and minify JSON. Syntax errors are reported with the exact l…
Open tool →