Notes › Tech Notes › Unix timestamps that do not match the date: seconds, milliseconds and time zones

Unix timestamps that do not match the date: seconds, milliseconds and time zones

At twenty past one in the morning, a group chat woke me up: someone in operations posted a dashboard screenshot where the entire Created At column for March orders had turned into 1970-01-21 00:13:20. The frontend developer on call got up and read the code, and found it within five minutes: the API returned create_time as a 10-digit seconds value, and his new Date(create_time) treated that number as milliseconds. The same 1700000000 lands in November 2023 when it is read as seconds and falls back to January 1970 when it is read as milliseconds. After that, our team wrote the timestamp conventions onto one card, pinned it to the top of the wiki, and new hires picking up a debugging task rarely need to ask anyone.

1. Digit count is the unit: learn to read it at a glance

A timestamp is just an integer, and what it means depends entirely on the unit it was produced in. My first check is always to count the digits rather than guess which language the backend uses:

DigitsUnitTypical sourceRelation to seconds
10 digitsSecondsPHP time(), Go Unix(), Linux date +%sBaseline
13 digitsMillisecondsJavaScript Date.now(), Java currentTimeMillis()Seconds x 1000
16 digitsMicrosecondsGo UnixMicro(), Python time_ns()//1000Seconds x 1000000
19 digitsNanosecondsPython time_ns(), Go UnixNano()Seconds x 10⁹

One more range check: between 2020-01-01 and 2030-01-01, seconds values fall between 1577836800 and 1893456000, and milliseconds between 1577836800000 and 1893456000000. Parse a 13-digit value as 10 digits and the date flies off to somewhere around the year 50000; read 10 digits as 13 and you drop back to 1970. Nearly every time display problem I have run into sits in one of those two buckets.

2. The missing 8 hours are not in the timestamp, they are in the layer that reads it

A timestamp is an absolute quantity counted from 1970-01-01 00:00:00 UTC and carries no time zone information at all. The same number is the same value in London and in Shanghai; the disagreement only appears at the step where the integer is translated into a date a person can read. Take 1700000000: its UTC time is 2023-11-14 22:13:20, and in Beijing time that is 2023-11-15 06:13:20, exactly 8 hours apart.

Four places in real projects account for most of the 8-hour illusions, and each is worth ruling out in turn:

  • The container defaults to UTC while the host runs Asia/Shanghai, so log wall-clock times sit 8 hours away from what the business sees. Run TZ=Asia/Shanghai date -d @1700000000 once with and once without TZ and you can tell which layer is wrong.
  • MySQL FROM_UNIXTIME() outputs in the session time zone. Run SELECT @@session.time_zone first, then cross-check in both directions against UNIX_TIMESTAMP(); the column itself is usually fine.
  • PHP without date_default_timezone falls back to UTC, so the output of date() is shifted as a whole.
  • On the frontend, new Date(ts).toISOString() always returns UTC with a trailing Z, while toLocaleString() renders in the system time zone. Data that looks inconsistent between two functions is the most common source of self-doubt while debugging.

3. The order I debug in, and where the tools come in

  1. Count the digits to fix the unit first, and write down the conclusion, seconds or milliseconds, so two people do not work from two different readings.
  2. Then fix the baseline: we compressed our rule into one sentence. The database stores only 10-digit UTC seconds, API field names carry the unit in a suffix (create_time_ms), and the presentation layer converts to local time.
  3. Verify in both directions: take a known moment (say 23:07 last night), convert it to a timestamp, convert it back, and see whether you land on the same value. This single step exposes both unit and time zone errors.
  4. For bulk checks I paste the suspicious numbers into Timestamp Converter. It works out seconds versus milliseconds on its own and shows Beijing time, UTC, ISO 8601 and relative descriptions such as 3 days ago, which saves me doing time zone offsets in my head. Going the other way, date to timestamp, happens on the same page, and it also carries the timestamp snippet for several languages, so switching languages is a matter of copying that block.
  5. To measure the gap between two moments, or to work out which date is 90 days from now, I use Time Calculator. It returns the weekday and the timestamp along with the result, which is more reliable than dividing 86400 by hand.

4. Edge cases that are easy to skip

These are the ones I only check after being burned, written down here as a list:

Edge caseWhat it looks likeWhat to do
32-bit overflowSigned 32-bit seconds go negative at 2038-01-19 03:14:07 UTC; the MySQL TIMESTAMP column caps at the same momentMove the column to BIGINT, and check the headroom with SELECT MAX() before migrating
Negative timestampsTimes before 1970 are negative, and some database functions and charting libraries show them blank or throw an errorStore historical archives as date strings and keep timestamps for recent decades only
Leap secondsTimestamps do not express leap seconds, so a scheduler aligned to UTC seconds can see a repeated second on a leap dayUse a monotonic clock (CLOCK_MONOTONIC) for critical timing, and do not count down with wall time
Daylight saving timeUS clocks jump from 02:00 to 03:00 on 2024-03-10, so that hour does not exist locally; the switch back on 11-03 repeats an hourStore IANA zone names such as America/New_York, not a fixed offset like UTC+4
Same-second collisionsUsing a timestamp as a filename or cache key means requests within the same second overwrite each otherAppend a random suffix at millisecond resolution, or use a UUID

Scheduling meetings across countries is a different need: what you actually want is usually not a timestamp but whether it is working hours for the other side right now. For that I first check the current time and offset for both cities in World Clock, and then convert the chosen moment into a timestamp for the calendar API.

5. Common questions

Q: The conversion is off by 8 hours. Is the timestamp wrong?

Almost never. Check the time zone used for parsing, then the digit count. The timestamp value has not changed; only the layer that renders it has.

Q: In JavaScript, should I multiply by 1000 or divide by 1000?

Look at the input digits. JavaScript Date only understands milliseconds, so a 10-digit value needs multiplying by 1000; going the other way, if you send Date.now() to an endpoint that parses seconds, divide by 1000 and round. The most robust fix is to put the unit in the field name.

Q: How do I tell quickly whether a messy batch of log times is in seconds or milliseconds?

Take the largest value and count its digits, then compare against the range: between 2020 and 2030, 10 digits is seconds and 13 is milliseconds. Mixing both in one log is not rare, so bucket them first and convert each group.

Q: Is the 2038 problem far away?

Not while 32-bit integer columns are still running on old systems. The cost is mostly changing column types and rebuilding indexes, it gets worse the longer you wait, and the best moment is the next table restructuring you were going to do anyway.

Looking back at that incident, the real fix was not that one new Date() line but the rule that whoever produces the number labels the unit, and whoever displays the time owns the time zone, finally landing in the API documentation. These days, when I see 1970 or the year 50000, I count the digits first and check the time zone second; with those two steps in order, most time confusion never reaches the second one.

📎 Related articles

PNG compressed three times only lost 426 bytes: the real boundary of lossless compression 2026-10-22 How Many Times Bigger a PNG Gets After Converting From WebP: When It Is Unavoidable and How to Keep the Growth Down 2026-10-22 Three Reasons the Small Text Smudges When a Screenshot Is Shrunk to a Quarter 2026-10-22 Cropped to the Same 300x300, Why Does the Avatar Always Sit Half a Cell Low-Right 2026-10-22 Forecasting 18 months at 1.7 times a month: why the script and the sheet give two numbers 2026-10-22