Unix timestamps: seconds vs milliseconds, zones and 2038

Tell seconds, milliseconds, microseconds and nanoseconds apart by digit count, convert epoch values in any time zone, read ISO 8601 and the 2038 problem.

On this page

A Unix timestamp is the number of seconds that have passed since 00:00:00 UTC on 1 January 1970, the epoch. Many systems count in smaller units instead, and the quickest way to tell them apart is the digit count of a present-day value: 10 digits is seconds, 13 is milliseconds, 16 is microseconds and 19 is nanoseconds. Read a millisecond value as seconds and you get a year around 55,000; read seconds as milliseconds and you get January 1970. This article explains the epoch, the units and the digit rule with its limits, shows why a timestamp has no time zone while its date does, covers ISO 8601, and ends with the year 2038 problem.

What the epoch is

1700000000 is the moment 22:13:20 UTC on 14 November 2023. Zero is the epoch, 1 January 1970 00:00:00 UTC, and negative numbers are earlier: -86400 is 31 December 1969. A timestamp is a single number for a single moment in the world, the same in every time zone, which is why databases, logs, APIs and file systems like it.

Unix time ignores leap seconds. It counts every day as exactly 86,400 seconds, so one timestamp corresponds to exactly one date and the other way round.

Seconds, milliseconds, microseconds, nanoseconds

The same moment as written by different systems, using 14 November 2023:

UnitValueDigitsWhere you meet it
Seconds170000000010Unix date +%s, PHP time(), JWT claims, many databases
Milliseconds170000000012313JavaScript Date.now(), Java System.currentTimeMillis()
Microseconds170000000012345616Some databases and tracing systems
Nanoseconds170000000012345678919Go UnixNano(), Python time.time_ns()

At the time of writing, the current time is about 1791895973 in seconds and 1791895973607 in milliseconds, so the 10 and 13 digit counts are what you will actually see. Python's time.time() returns seconds with a fraction, as in 1700000000.5.

The digit rule, and where it stops working

Count the digits of the whole part. The converter below reads up to 11 digits as seconds, 12 to 14 as milliseconds, 15 to 17 as microseconds and 18 or more as nanoseconds. Those cut-offs are chosen so that the rule is right for dates from about 1973 to the year 2286:

  • Ten-digit seconds run from 1000000000 (9 September 2001) to 9999999999 (20 November 2286). Eleven digits stretch it to the year 5138.
  • Thirteen-digit milliseconds run from 1000000000000 (also 9 September 2001) on. Twelve-digit milliseconds go back to 100000000000, which is 3 March 1973.

The rule fails for old dates. A millisecond timestamp from 1972 has 11 digits, and a tool that goes by digit count reads it as seconds in the year 5138. Likewise a seconds timestamp before September 2001 has only 9 digits. If you work with historical data, choose the unit yourself instead of trusting auto-detection.

The two classic mistakes are symmetrical:

What you didResult for 1700000000 or 1700000000000
Read milliseconds as seconds1700000000000 seconds is about the year 55,841
Read seconds as milliseconds1700000000 ms is 20 January 1970, 16:13:20 UTC

Seeing 1970 where you expected a recent date, or a year in the tens of thousands, points to this mistake in one step.

A timestamp has no time zone, a date does

The same 1700000000 shows up as different clock times depending on where you are:

ZoneOffsetDate and time
UTC+00:0014 Nov 2023, 22:13:20
Ho Chi Minh City+07:0015 Nov 2023, 05:13:20
New York-05:0014 Nov 2023, 17:13:20
Tokyo+09:0015 Nov 2023, 07:13:20

Note that in Ho Chi Minh City it is already the next day. A bug that reports "the wrong day" is usually a timestamp converted in the wrong zone. Store and exchange the timestamp, or a UTC date, and apply the zone only when you display a value.

ISO 8601 in practice

ISO 8601 is the date format meant to avoid ambiguity: year first, then month, day, T, then the time and the offset. The same moment can be written two ways:

2023-11-14T22:13:20Z
2023-11-15T05:13:20+07:00

Z means UTC, and +07:00 means seven hours ahead of UTC. Both are exactly 1700000000. A date-time without a Z or an offset is ambiguous: it names a clock reading, not a moment. JavaScript adds a quirk of its own: new Date("2023-11-15") is read as midnight UTC, while new Date("2023-11-15T00:00:00") is read as midnight in the local time zone. On a computer set to Ho Chi Minh City time, the second one is 17:00 UTC on the 14th. Always write the offset when the date is going to another system.

Daylight saving: times that do not exist

Where clocks change, some clock readings never happen and some happen twice. In New York on 10 March 2024 the clocks jumped from 02:00 to 03:00, so 2024-03-10 02:30 never existed; the converter moves it forward to 03:30 and says so. On 3 November 2024 the clocks went back from 02:00 to 01:00, so 01:30 happened twice, at 05:30 UTC and again at 06:30 UTC; the converter uses the earlier one and says so. The lesson for schedules and logs is to keep the underlying timestamp in UTC. The cron expression cheat sheet shows how the same effect hits scheduled jobs.

The year 2038 problem

A signed 32-bit integer holds values up to 2,147,483,647. As a timestamp in seconds, that is 2038-01-19 03:14:07 UTC. One second later the counter wraps around to -2,147,483,648, which is 13 December 1901, 20:45:52 UTC. Systems that still keep seconds in a 32-bit signed field, such as old embedded devices, some file formats and database columns declared as a 32-bit integer, would then show dates from 1901.

An unsigned 32-bit counter lasts until 7 February 2106, 06:28:15 UTC. A 64-bit counter makes the problem go away for any practical purpose: it overflows about 292 billion years from now. The fix is to use 64-bit time values, and to check the type of every column and field that stores a timestamp. Milliseconds in a 32-bit field overflow much sooner, in less than 25 days after the epoch.

Converting with the tool

  1. Open the timestamp converter. Type or paste a value in the one box: a number is read as a timestamp, a date is read as a date. With the box empty it shows the current time and updates every second; switch "Live clock" off to freeze it, or press Now.
  2. Check the note under the box, such as "Read as milliseconds (13 digits)". If it guessed the unit wrong, choose Seconds, Milliseconds, Microseconds or Nanoseconds under "Timestamp unit".
  3. Pick the Time zone. The default is your device's. A date typed without an offset is read in the chosen zone; one with Z or an offset such as +07:00 carries its own.
  4. Copy any row: Unix seconds, milliseconds, microseconds, nanoseconds, ISO 8601 in UTC and in the zone, RFC 2822, a readable form, and a relative time such as "3 days ago".

It accepts years 1 to 9999 and does all the maths in your browser, using the time zone data your browser already has. A JWT's exp is such a seconds value; how to read a JWT puts it to use. Time-ordered identifiers carry a millisecond timestamp too, as the UUID v4, v7 and ULID comparison shows.

Command-line and code recipes

date +%s                         # now, in seconds
date -u -r 1700000000            # macOS: seconds to date, in UTC
date -u -d @1700000000           # GNU/Linux: the same
Date.now();                                  // milliseconds
new Date(1700000000 * 1000).toISOString();   // "2023-11-14T22:13:20.000Z"
Math.floor(Date.now() / 1000);               // seconds

In a spreadsheet, =A2/86400+25569 turns a timestamp in seconds into a date once the cell is formatted as a date, because 25569 is the number of days between Excel's day zero and 1970.

A short checklist

Count the digits before you convert, and choose the unit yourself for dates before 1973. Keep stored times as UTC timestamps and apply a time zone only when showing them. Write ISO 8601 with an offset when exchanging dates. Be careful with clock readings around daylight saving changes, and use 64-bit fields for anything that must work after January 2038.

Tools mentioned in this article

Back to the blog