⏱️ Coding

Unix Time: Seconds, Milliseconds, and the Bugs in Between

Unix time counts seconds since 1970. JavaScript counts milliseconds. Almost every mysterious date bug is one of those two being mistaken for the other.

A Unix timestamp is a single integer: the number of seconds since 1 January 1970, 00:00:00 UTC. No timezone, no calendar, no formatting. That simplicity is why it is the standard interchange format for a moment in time, and why so many bugs cluster around it.

Most of those bugs come from one of three things: the unit, the timezone that is not there, and the assumption that the count is pure elapsed time. All three are worth understanding before the next time a date is off by a suspiciously round amount.

Seconds or milliseconds

The Unix convention is seconds. JavaScript's Date.now() returns milliseconds. That factor of 1000 is the single most common date bug in web development.

The giveaway is the magnitude. A current timestamp in seconds has 10 digits; in milliseconds, 13. If you see a date in 1970 where you expected today, you passed seconds to something wanting milliseconds. If you see a date tens of thousands of years in the future, you did the reverse.

  • 1767225600 — seconds. 1 January 2026.
  • 1767225600000 — milliseconds. The same moment.
  • new Date(1767225600) — 20 January 1970. Wrong.
  • new Date(1767225600 * 1000) — correct.

This is exactly the trap in JWT expiry checks: exp is in seconds, so comparing it to Date.now() makes every token look expired. Compare against Math.floor(Date.now() / 1000).

Other systems use other units. Java's System.currentTimeMillis() is milliseconds; Python's time.time() is a float of seconds; Go's UnixNano() is nanoseconds; Windows FILETIME counts 100-nanosecond intervals since 1601. When a timestamp crosses a system boundary, the unit is the first thing to pin down.

A timestamp has no timezone

This confuses people in both directions. A Unix timestamp identifies an unambiguous instant. It is not in UTC in the sense of being convertible to some other timestamp for another zone — there is only one number for a given moment, worldwide.

The timezone enters only when you display it. Timestamp 1767225600 is:

  • 00:00 on 1 Jan 2026 in London (UTC+0)
  • 05:30 on 1 Jan 2026 in Kolkata (UTC+5:30)
  • 19:00 on 31 Dec 2025 in New York (UTC−5)

Same instant, three different dates. Note that the New York rendering is in a different year, which is how "group events by day" quietly becomes a timezone question and why reporting dashboards disagree with each other.

The practical rule: store timestamps, format on display. Store the integer, keep the user's zone separately, and convert only at the edge. Storing a formatted local string throws away the information needed to re-render it correctly, and storing "the date" without a zone is ambiguous by up to 26 hours once you account for the full offset range.

Leap seconds, and why it is not elapsed time

Unix time is defined as if every day has exactly 86,400 seconds. The Earth does not cooperate: its rotation varies, and leap seconds are inserted to keep clock time aligned with it. Twenty-seven have been added since 1972.

Unix time does not count them. When a leap second occurs, the timestamp either repeats a value or is smeared across a longer interval, depending on the implementation. The consequence is that the difference between two Unix timestamps is not the number of seconds that actually elapsed between them — it is short by the number of leap seconds in the interval.

For nearly all purposes this does not matter; 27 seconds across half a century is irrelevant to a session expiry. It matters for precise interval measurement, and it is why monotonic clocks exist: performance.now() in the browser, CLOCK_MONOTONIC on Linux. Use a monotonic clock to measure durations, and wall-clock timestamps to record when something happened. Measuring an elapsed duration with wall-clock time also means an NTP correction or a daylight-saving change can produce a negative duration.

A related curiosity: the leap second scheduled to be removed has never happened, and in 2022 the relevant bodies agreed to stop adding leap seconds by 2035. The problem is slowly being retired rather than solved.

2038, and the other boundaries

A signed 32-bit integer tops out at 2,147,483,647. As a Unix timestamp that is 03:14:07 UTC on 19 January 2038. One second later it overflows to negative, and the date reads 13 December 1901.

This is the Year 2038 problem, and it is structurally the same as Y2K: a storage width chosen when the horizon seemed distant. 64-bit time is the fix, and most modern systems use it — the remaining exposure is in embedded devices, old file formats, and database columns typed as a 32-bit integer years ago. A 64-bit timestamp runs out in about 292 billion years.

Two other boundaries are worth knowing because they show up as data, not as outages. Zero is 1 January 1970, and a field that renders as that date almost always means "null that was coerced to 0" rather than a real 1970 date. And negative timestamps are valid and represent dates before 1970 — but plenty of code assumes non-negative, so a birthdate in 1965 is a reliable way to find out whether a system handles them.

Working with timestamps without tripping over them

A few habits that prevent most of the above:

  • Name the unit in the variable. expiresAtSeconds and createdAtMs make the mismatch visible at the call site, where timestamp hides it.
  • Store UTC, display local. Never store a formatted local time as the source of truth.
  • Store an IANA zone name, not an offset. Asia/Kolkata survives daylight-saving rule changes; +05:30 does not, and is wrong for half the year in most zones.
  • Use a monotonic clock for durations. Wall-clock differences can go backwards.
  • Sanity-check the digit count. Ten digits is seconds, thirteen is milliseconds. This catches the problem in two seconds rather than twenty minutes.

And when debugging, convert the suspicious number by hand before theorising. A date in 1970 means seconds-for-milliseconds. A date in 1901 means 32-bit overflow. A date in the year 55,000 means milliseconds-for-seconds. The wrong date usually names its own cause.

Converting in the browser

The Unix timestamp converter goes both ways, handles seconds and milliseconds, and shows the result in UTC and in your local zone side by side — which is the quickest way to see whether an off-by-a-few-hours problem is a timezone issue or something else.

For arithmetic on dates rather than instants — how many working days until a deadline, how old something is in months — the date calculator is the better fit, because those are calendar questions rather than timestamp ones, and the distinction is real: adding "one month" to 31 January has no timestamp answer.

If the timestamp you are chasing came out of a token, the JWT decoder converts exp, iat and nbf for you and reads them as seconds, which is what they are.

Frequently asked questions

Is a Unix timestamp in seconds or milliseconds?

The Unix convention is seconds; JavaScript's Date.now() returns milliseconds. Count the digits — a current timestamp is 10 digits in seconds and 13 in milliseconds. A date in 1970 means you passed seconds where milliseconds were expected.

What timezone is a Unix timestamp in?

None. It identifies a single worldwide instant. The timezone only applies when formatting it for display, which is why the same timestamp can render as a different date — sometimes a different year — in two places.

What is the Year 2038 problem?

A signed 32-bit integer maxes out at 2,147,483,647 seconds, which is 03:14:07 UTC on 19 January 2038. One second later it wraps to a negative value reading as December 1901. 64-bit time stamps fix it and most systems already use them; the exposure is in embedded devices and old integer columns.

Does the difference between two timestamps equal the elapsed seconds?

Not exactly. Unix time treats every day as 86,400 seconds and ignores the 27 leap seconds added since 1972, so the difference is short by however many fall in the interval. Use a monotonic clock, such as performance.now(), to measure durations precisely.

Should I store a UTC offset or a timezone name?

A timezone name such as Asia/Kolkata. An offset like +05:30 is a snapshot that becomes wrong when daylight-saving rules change or apply, and the rules change more often than people expect.

Everything on ToolYard runs in your browser. No uploads, no accounts, no limits.

Browse all tools →