A Unix timestamp is the simplest date format there is: one integer, counting from midnight UTC on 1 January 1970. Nearly every bug it causes comes from the two things that integer leaves out, which are the unit it counts in and the time zone a person wants to read it in.
What the Number Counts
POSIX defines Unix time with a formula rather than a clock. It takes a UTC calendar date and time and computes seconds + minutes × 60 + hours × 3600 + days × 86400, where the days are counted from 1970 with the Gregorian leap year rules applied. Every day in that formula is exactly 86,400 seconds long.
That one decision explains most of Unix time's behaviour. It is why converting a timestamp to a date needs no lookup table, only arithmetic. It is also why the count does not include leap seconds, since a day that is defined as 86,400 seconds has no room for an 86,401st.
It also means the number carries no time zone. 1700000000 is 22:13:20 in London, 07:13:20 the next morning in Tokyo and 17:13:20 in New York, all at the same moment. Time zones only appear when you turn the count back into a calendar date for someone to read.
The Digit Count Tells You the Unit
Every timestamp from 9 September 2001 to 20 November 2286 has the same number of digits within each unit, which is why counting digits works as a test.
| Digits | Unit | 1 billion seconds after the epoch looks like | Typical source |
|---|---|---|---|
| 10 | Seconds | 1000000000 | date +%s, MySQL UNIX_TIMESTAMP(), JWT exp and iat, Stripe |
| 13 | Milliseconds | 1000000000000 | JavaScript Date.now(), Java currentTimeMillis(), Kafka, Elasticsearch epoch_millis |
| 16 | Microseconds | 1000000000000000 | BigQuery UNIX_MICROS(), Parquet timestamp columns, Slack ts with the dot removed |
| 19 | Nanoseconds | 1000000000000000000 | Go UnixNano(), Python time.time_ns(), InfluxDB line protocol |
What the Wrong Unit Looks Like
The two mistakes are easy to recognise once you have seen them, because they miss by a factor of a thousand in one direction or the other.
Milliseconds read as seconds throws the date into the far future. 1700000000000 taken as seconds lands in the year 55,840. This is the classic symptom of passing Date.now() into a field specified in seconds, such as a JWT's exp, and it makes a token that never expires.
Seconds read as milliseconds collapses the date into the first weeks of 1970. 1700000000 taken as milliseconds is 20 January 1970 at 16:13:20 UTC. If a dashboard shows every event clustered in January 1970, something upstream is sending seconds into a JavaScript new Date(), which expects milliseconds.
The converter on this site applies the same reasoning in reverse. Anything below 100 billion is read as seconds, since 100 billion seconds is the year 5138, while 100 billion milliseconds is March 1973. Each step up uses the same cut-off, so a value is always given the reading that lands it closest to now.
Why JSON Loses Your Nanoseconds
A JavaScript number is a 64-bit float, and it stores integers exactly only up to 2^53, which is 9,007,199,254,740,991. That is plenty for milliseconds, which stay exact until the year 287,396. Microseconds stay exact until 2255.
Nanoseconds run out on 15 April 1970. Every nanosecond timestamp after that is too large for a double to hold exactly, and the parser rounds it to the nearest value it can represent. Run JSON.parse("1700000000123456789") in a browser console and you get back 1700000000123456800. The last two digits are gone and nothing reports it.
This bites in practice whenever a Go or Rust service emits UnixNano() into a JSON API and a JavaScript client reads it. Two events 20 nanoseconds apart can come out with the same value, and an id built from the timestamp can collide. The fixes are to send the value as a string, to send it in microseconds, or to parse it with BigInt on the client. The converter here reads the digits as a BigInt for exactly this reason, so a pasted 19-digit value keeps all 19.
The leap second that took down a DNS resolver
At midnight UTC on 1 January 2017 a leap second was inserted, and the clock read 23:59:60. Unix time cannot express that second, so most systems repeated the previous one. Cloudflare's DNS software measured a duration by subtracting two clock readings, got a negative number across the repeat, and passed it to a random number function that panics on negative input. Some DNS lookups failed until the code was patched. Their postmortem is the reason many engineers now reach for a monotonic clock when measuring elapsed time, and why Google, Amazon and Meta smear a leap second across many hours rather than repeating one.
Where 2038 Still Lives
A signed 32-bit integer tops out at 2,147,483,647. As a Unix timestamp that is 03:14:07 UTC on Tuesday 19 January 2038. One second later the value wraps to −2,147,483,648, which reads as 20:45:52 on 13 December 1901.
Desktop and server operating systems moved to a 64-bit time_t years ago, and a 64-bit count of seconds lasts about 292 billion years. Linux finished the job for 32-bit hardware in kernel 5.6, released in 2020, and glibc 2.34 lets 32-bit programs opt in with _TIME_BITS=64. What remains is the long tail:
Database columns. MySQL's TIMESTAMP type still stores four bytes and accepts nothing after 2038-01-19 03:14:07 UTC. DATETIME does not have the limit. If a table holds expiry dates, subscription end dates or mortgage terms, check which one it uses now, because a 30-year date already crosses the line.
File formats and protocols. Any binary format with a 32-bit seconds field inherits the limit, whatever the software reading it does. An unsigned 32-bit field buys time until 7 February 2106.
Embedded devices. Routers, cars, industrial controllers and medical equipment often run 32-bit firmware that will never be updated, which is where the 2038 problem is most likely to cause real failures.
The Limits Worth Knowing
Each boundary is the largest value the type can hold, converted to UTC.
| Storage | Last representable moment | Where you meet it |
|---|---|---|
| Signed 32-bit seconds | 19 January 2038, 03:14:07 | Old C code, MySQL TIMESTAMP, 32-bit firmware |
| Unsigned 32-bit seconds | 7 February 2106, 06:28:16 | Some file formats and network protocols |
| Signed 64-bit nanoseconds | 11 April 2262, 23:47:16 | Go time.UnixNano(), pandas Timestamp |
| JavaScript Date | 13 September 275,760 | Every browser and Node.js |
| Signed 64-bit seconds | About 292 billion years from now | Modern time_t on Linux, macOS, Windows |
Convert a timestamp in any unit
Seconds to nanoseconds, any time zone, and dates back to timestamps.
Converting a Local Time Back to a Timestamp
Going from a timestamp to a date always has one answer. Going from a local date and time back to a timestamp does not, because daylight saving changes make the mapping between clock time and real time uneven.
When the clocks go back in autumn, an hour happens twice. In New York, 01:30 on 3 November 2024 occurred first at 1730611800 and again an hour later at 1730615400. A form that asks a user for a local time and stores one timestamp has to pick one of the two, and most libraries pick the earlier.
When the clocks go forward in spring, an hour never happens at all. 02:30 on 10 March 2024 did not appear on any New York clock. Libraries disagree about what to do with it: some throw an error, and others move it forward by the length of the gap, so 02:30 becomes 03:30. The JavaScript Temporal API calls the second behaviour compatible and makes it the default, and the converter on this site follows it and tells you when it happened.
The extreme case is Samoa, which moved across the International Date Line at the end of 2011. The whole of 30 December 2011 was skipped. Clocks went from 23:59:59 on the 29th straight to 00:00:00 on the 31st.
The practical rule follows from all of this. Store instants as timestamps or as UTC, and store the user's time zone name, such as Europe/Berlin, next to anything that has to happen at a local clock time. A fixed offset like +02:00 is not a time zone. It is only correct until the next daylight saving change.
Habits That Prevent Most Timestamp Bugs
None of these need a library. They need the unit and the zone to be written down somewhere other than a developer's head.
Put the unit in the name
A column called created_at_ms or a field called expires_unix_s cannot be misread. A field called timestamp will be, by someone, eventually.
Exchange ISO 8601 at the edges
Inside a system an integer is compact and fast to compare. In an API meant for other people, 2023-11-14T22:13:20Z is self-describing, sorts correctly as text and carries its own precision.
Measure durations with a monotonic clock
Wall-clock time can jump backwards when NTP corrects it or a leap second repeats. performance.now() in a browser, time.monotonic() in Python and time.Since in Go cannot, so a duration measured with them is never negative.
Send nanoseconds as strings
Any 19-digit number that passes through JavaScript, or through a JSON library that parses into doubles, comes out rounded. A string survives every hop unchanged.
Keep the zone name, not the offset
An offset describes one moment. A zone name from the tz database describes the rules, including every past and future daylight saving change, which is what a recurring local event needs.
Frequently Asked Questions
Related Tools
Keep Reading
Inside a JWT: Why Decoding a Token Proves Nothing
The claims are base64, not encryption, so the holder can read every one of them. What the signature settles, and the checks it quietly leaves to you.
Which UUID Version to Use, and Why v7 Changed the Answer
A random primary key splits B-tree pages and scatters an InnoDB table. RFC 9562 fixed that in 2024 by putting the clock first. What each version really holds.
Date Arithmetic Is Harder Than It Looks: Leap Years and Feb 29
One month after 31 January has no single right answer. How months create ambiguity, and when a leap-day baby legally turns 18.
Where Hex Converters Quietly Round Off Your 64-Bit Numbers
FFFFFFFFFFFFFFFF is 18446744073709551615, but a lot of tools say ...552000. The 53-bit significand behind it, and why all ones means minus one.