What is a Unix Timestamp?

A Unix timestamp counts seconds since 00:00:00 UTC on 1 January 1970, excluding leap seconds. That origin is called the epoch. The value is a plain integer carrying no timezone, no format, and no locale: the same number describes the same absolute instant on every machine in every country.

That property is why logs, databases, cache expiry values, and API fields such as created_at carry timestamps instead of formatted date strings. A string has to be parsed before it can be compared, needs a separate timezone field, and means different things in different locales. An integer sorts directly, differences are meaningful, and it can be indexed.

One consequence is easy to miss: Unix time contains no leap seconds. Every day is exactly 86400 seconds long even when the physical day was 86401 seconds, which is what makes "add one day = add 86400 seconds" exact.

Seconds versus Milliseconds

The most common conversion mistake is unit confusion, not timezones. Second-level values in the 2020s have 10 digits; millisecond-level values have 13. Mixing them puts the result a factor of 1000 off, which shows up either as a date in 1970 or a five-digit year.

Recognising the unit by digit count
UnitTypical lengthExample valueIf misread as the other unit
Seconds (10 digits)10 digits1790090593Read as ms → 1970-01-21
Milliseconds (13 digits)13 digits1790090593958Read as s → year 58695

Forcing a unit manually has a second use beyond double-checking: when a system emits non-standard precision — microseconds, or a counter in half-second steps — automatic detection guesses wrong, and switching the unit by hand is how you work out what the producer actually meant.

Why the 10^11 Boundary Exists

The tool treats any value at or above 100000000000 (10^11) as milliseconds and anything below it as seconds. The constant is not arbitrary; it is derived from how long a second-level timestamp would take to reach that size.

10^11 seconds is roughly 3169 years, so a second-level timestamp that large would not appear until the year 5138. But 10^11 milliseconds is only about 1157 days, which lands on 3 March 1973 — a millisecond value that has existed for half a century. For any foreseeable future, everything above the line is milliseconds and everything below is seconds.

How values around the boundary are classified
InputDigitsClassified asResult (UTC)Why
9999999999911Seconds5138-11-16T09:46:39ZBelow the boundary
10000000000012Milliseconds1973-03-03T09:46:40ZEqual to the boundary, so ms wins
10000000000112Milliseconds1973-03-03T09:46:40.001ZOne millisecond past it

Note the tie-breaking: the boundary value itself is read as milliseconds, because the test is "greater than or equal". And 99999999999, despite having only 11 digits, falls below the line and is read as seconds — producing the year 5138, which looks absurd but demonstrates that the threshold sits comfortably high.

Landmark Timestamps

The values below work as reference points for sanity-checking magnitude and estimating an unfamiliar timestamp by eye.

Reference instants (UTC)
MomentSecondsMillisecondsUTC rendering
Unix epoch001970-01-01T00:00:00Z
One second before-1-10001969-12-31T23:59:59Z
Start of 20009466848009466848000002000-01-01T00:00:00Z
Start of 2020157783680015778368000002020-01-01T00:00:00Z
Start of 2026176722560017672256000002026-01-01T00:00:00Z
32-bit signed limit2147483647Overflows2038-01-19T03:14:07Z

Two constants cover most mental arithmetic: one day is 86400 seconds and a common year is 31536000 seconds. Dividing a timestamp by 86400 gives whole days since the epoch; the remainder is seconds since midnight UTC.

Worked Examples

  1. Seconds to a readable date: 1600000000 resolves to 2020-09-13T12:26:40Z, which is 2020-09-13 20:26:40 at UTC+8. Dividing by 86400 gives 18518.518 days and the remainder is 44800 seconds — exactly 12:26:40 — matching the tool output.
  2. Milliseconds to the same instant: 1600000000000 is auto-detected as milliseconds and yields the same 2020-09-13T12:26:40Z. Two values a thousand times apart describing one instant is the clearest demonstration that seconds and milliseconds are two graduations on a single axis.
  3. What a unit mistake looks like: reading 1600000000 as milliseconds gives 1970-01-19T12:26:40Z — the classic "it jumped back to 1970" report. Reading 1600000000000 as seconds gives the year +057971. Counting the digits before you convert rules out both.
  4. Date back to timestamp: picking 2026-01-01 00:00:00 UTC yields 1767225600 seconds and 1767225600000 milliseconds. Adding 28800 seconds (the UTC+8 offset) to the same instant yields 2026-01-01 08:00:00 Beijing time.
  5. Negative values: -1 gives 1969-12-31T23:59:59Z, one second before the epoch. Negative timestamps are not errors; they are legitimate pre-1970 instants and show up when auditing old log files.
  6. Elapsed-day arithmetic: 2026-10-09T00:00:00Z is 1791504000, and dividing by 86400 divides exactly to 20735 days. To compute "days ago", subtract the target from the current timestamp before dividing, and round down rather than to nearest, otherwise a 23-hour-old gap displays as "1 day ago".

The 2038 Problem

The canonical Unix timestamp type is a signed 32-bit integer, and its maximum value 2147483647 corresponds to 2038-01-19T03:14:07Z. One second later the counter wraps negative and the date falls back into 1970. This is the Year 2038 problem, and the fix is 64-bit storage: a signed 64-bit integer reaches about 9.22 × 10^18 seconds, comfortably past the year 290 million. Modern language and database date types are 64-bit, so the survivors are older embedded devices, legacy file formats, and database columns still typed int32. Migrate those before 2038 rather than after.

Timezones and Offsets

A timestamp is timezone-independent; the offset belongs to the display layer. The same instant rendered in three zones gives 12:26:40 at UTC, 20:26:40 at UTC+8, and 07:26:40 at UTC−5 for 1600000000 — the number never changes, only the wall clock does.

Offsets are whole seconds, so timezone arithmetic is simple addition: UTC+8 adds 28800 seconds, UTC+9 adds 32400, UTC−5 subtracts 18000. Fractional zones follow the same rule — India at UTC+5:30 is 19800 seconds and Nepal at UTC+5:45 is 20700. This is exactly why servers and databases store offsets in minutes rather than hours.

Daylight saving is the complication worth planning for. In regions that observe it, the offset changes twice a year, so "28800 seconds" is stable for China but not for Europe or North America. The safe pattern is to store UTC and convert only for display rather than baking a local offset into stored data.

Where Leap Seconds Went

Leap seconds exist because Earth's rotation is not uniform, and UTC has inserted them repeatedly. Unix time never does: it fixes every day at 86400 seconds even when the real physical day was 86401 seconds long.

The leap seconds are not lost — under POSIX convention they are folded into a neighbouring second, usually the one before or the one after, and each system picks its own placement. So a Unix timestamp is not a count of elapsed SI seconds: the timeline is uniform seconds, and it can sit tens of seconds away from atomic time. For orders, logs, and monitoring that gap is irrelevant; for tidal, clock-discipline, or atomic-clock work it is a real caveat.

Converting Without a Tool

Two constants cover most quick checks: one day is 86400 seconds and a common year is 31536000 seconds. Dividing a timestamp by 86400 gives the number of days since the epoch, and the remainder is the time of day in seconds — so the hour is the remainder divided by 3600. For 1600000000 that yields 18518 days plus 44800 seconds, or 12:26:40 UTC.

Offsets are then simple additions: UTC+8 adds 28800 seconds and UTC−5 subtracts 18000, with half-hour and three-quarter-hour zones following the same rule. That is why most systems store offsets in minutes rather than hours.

Common zone offsets in seconds
ZoneOffset secondsApplied to 1600000000 (UTC)
UTC02020-09-13 12:26:40
UTC+8 (China)+288002020-09-13 20:26:40
UTC+9 (Japan)+324002020-09-13 21:26:40
UTC+5:30 (India)+198002020-09-13 17:56:40
UTC−5−180002020-09-13 07:26:40

Common Timestamp Formats

The same instant can be written several ways. Unix time is the storage-friendly form; ISO 8601 is the interchange form used by most APIs; HTTP-date appears in protocol headers. Converting between them does not change the instant, only the notation.

One instant (1600000000) in four notations
FormatValueTypical use
Unix seconds1600000000Databases, logs, sorting
ISO 8601 / RFC 33392020-09-13T12:26:40ZJSON APIs, config files
HTTP-date (RFC 7231)Sun, 13 Sep 2020 12:26:40 GMTHTTP headers, caching
Human (UTC)2020-09-13 12:26:40 UTCReports, user interfaces

Where 10 Digits and 13 Digits Come From

Different stacks made different choices. Unix time(), Java's System.currentTimeMillis(), and MySQL's UNIX_TIMESTAMP() all return seconds, so they yield 10-digit values. JavaScript's Date.now(), .NET's DateTimeOffset.ToUnixTimeMilliseconds(), and Go's time.Now().UnixMilli() return milliseconds, yielding 13-digit values. When two systems both store "user registered at" and disagree by a factor of 1000, the API contract simply never specified the unit. Documenting the unit in the field name — created_at_ms — prevents the whole class of bug.

Code-Level Pitfalls

The browser and Node both work in milliseconds internally, so a second-level timestamp must be multiplied before it is handed to the Date constructor: new Date(1700000000) yields 1970-01-20, while new Date(1700000000 * 1000) correctly gives 2023-11-14T22:13:20Z. Go the other way with Math.floor(Date.now() / 1000). Python defaults the other way, which is why cross-language bugs cluster here: datetime.datetime.utcfromtimestamp(1700000000) gives 2023-11-14T22:13:20Z, time.time() is a float in seconds, and int(time.time() * 1000) is the millisecond form.

Database column types are the next trap. MySQL's TIMESTAMP stores seconds with timezone conversion applied on write and read, so it carries UTC semantics; DATETIME stores the literal date and time with no conversion, which suits opening hours or birthdays. Pushing a 13-digit millisecond value into a TIMESTAMP column exceeds its range and is silently truncated or rejected — a frequent cause of "the time is wrong" reports that never mention milliseconds at all. JDBC types such as java.sql.Timestamp add their own millisecond-versus-nanosecond precision differences on top.

The remaining trap is treating a wall-clock time as UTC. A datetime-local input yields a naive local time with no zone attached, so something has to decide which zone it means before it is stored as a timestamp. Skipping that step is what produces a drift of several hours in cross-timezone features. Converting the suspect value back to a readable date and comparing it line by line against the log and the database row is the fastest way to localise the mismatch.

Practical Notes

Frequently Asked Questions

Why do some APIs return 10 digits and others 13?

Because they come from different runtimes. Unix time(), Java's System.currentTimeMillis(), and MySQL's UNIX_TIMESTAMP() produce seconds (10 digits), while JavaScript's Date.now(), .NET's ToUnixTimeMilliseconds(), and Go's UnixMilli() produce milliseconds (13 digits). Storing both in one column without recording the unit produces values that differ by 1000×. Name the unit explicitly — created_at_ms — or document it in the API contract.

Where does the 10^11 boundary come from, and how are edge values classified?

The constant is derived by asking how long a second-level timestamp would take to reach 1e11: roughly 3169 years, so the year 5138. Meanwhile 1e11 milliseconds is only about 1157 days — 3 March 1973 — a value that has existed for decades. Hence "≥ 1e11 means milliseconds". At the edge: 99999999999 sits just below and is read as seconds, giving 5138-11-16T09:46:39Z; 100000000000 equals the boundary and is read as milliseconds, giving 1973-03-03T09:46:40Z; 100000000001 is read as milliseconds, giving 1973-03-03T09:46:40.001Z. If your data really does contain a millisecond timestamp below 1e11 (anything before 1973), automatic detection fails and you must force the unit.

Why does Unix time exclude leap seconds, and where did they go?

Unix time's design goal is that a day is always 86400 seconds, which keeps date arithmetic deterministic; inserting a leap second would break that invariant. The seconds are not discarded — POSIX folds each leap second into an adjacent second, usually the preceding or following one, and leaves the exact placement to each implementation. A Unix timestamp therefore counts uniform seconds and can drift tens of seconds from atomic time. That gap is irrelevant for everyday scheduling but matters for timestamping, clock discipline, and any comparison against a reference clock.

What do MySQL's DATETIME and TIMESTAMP actually store?

TIMESTAMP stores seconds since the epoch with automatic timezone conversion on both write and read, spanning roughly 1970 to 2038 — it carries UTC semantics. DATETIME stores the literal date and time with no conversion and reaches back to the year 1000, which makes it the right choice for data with no timezone meaning such as business hours. For precision plus range, use DATETIME(3) or store milliseconds in a BIGINT. Writing a 13-digit millisecond value into a TIMESTAMP column overruns the supported range and is truncated or rejected outright.

Why does new Date(1700000000) need dividing by 1000 in Node?

JavaScript's Date represents instants in milliseconds internally, and the constructor argument is milliseconds too, so 1700000000 is interpreted as a moment in January 1970 rather than 2023. Use new Date(1700000000 * 1000) for 2023-11-14T22:13:20Z, and Math.floor(date.getTime() / 1000) to go back to seconds. Python inverts the convention, which is why this is such a common cross-language bug: time.time() returns float seconds, datetime.utcfromtimestamp() assumes seconds, and only the multiplied form gives milliseconds.

What should I watch for when turning a timestamp into "N days ago"?

Subtract the current timestamp first — dividing the target alone tells you its distance from the epoch, not from now. Then round down: a gap of 23 hours and 59 minutes is not yet a day, and rounding to nearest would display it as "1 day ago". Across a daylight saving transition one day is not 86400 seconds — it is 23 or 25 hours in local time — so fixed-86400 arithmetic drifts by an hour twice a year. And if you anchor to a fixed baseline such as "today at midnight", make sure the baseline uses the same timezone convention as the target, otherwise a morning entry already reads as a full day elapsed.

Related Reading

For day and week arithmetic around a given date, use the Date Calculator; for background on offsets and working-day rules, see our date calculation guide.