← 全部文章

每個開發者都發布過的 7 個 Unix 時間戳 bug

這些是引發生產事故的時間戳錯誤:悄無聲息的單位不匹配、時區假設、被遺忘的 ×1000、字串儲存、有歧義的解析,以及日光節約時間邊界錯誤。每個 bug 都附帶症狀、根因和實用修復。

Bug 1:JavaScript 中漏了 × 1000

new Date(1700000000) 產生的是接近 1970 年 1 月的日期,而非 2023 年 11 月。JavaScript 的 Date 建構函式期望毫秒,但大多數伺服器 API 和資料庫回傳 Unix 秒。這個 bug 常先出現在管理後台,因為後端負載是有效的 JSON,而介面只是算繪出了錯誤的年份。

  • Wrong: new Date(response.created_at) — if created_at is Unix seconds
  • Right: new Date(response.created_at * 1000)
  • Detection: if your date shows a year near 1970, the timestamp is in seconds not milliseconds
  • Safe wrapper: const toDate = ts => new Date(ts < 1e11 ? ts * 1000 : ts)

Bug 2:系統邊界處單位混用

JavaScript 回傳毫秒。Python、Go 和大多數伺服器端 API 回傳秒。當 JavaScript 未經轉換就把時間戳傳送給 Python 後端時,後端會把毫秒值解讀為秒,把事件放到 55792 年。反方向同樣糟糕:介面收到秒,直接傳給 new Date(),顯示 1970 年 1 月。

  • JavaScript sends: Date.now() → 1700000000000 (ms)
  • Python receives: datetime.fromtimestamp(1700000000000) → year 55792
  • Fix: divide by 1000 before sending to a non-JavaScript system
  • Better fix: use ISO 8601 strings at API boundaries — they are unambiguous

Bug 3:依賴伺服器的本地時區

使用 Python 的 datetime.fromtimestamp() 而不指定 UTC 的程式碼,會因伺服器設定的時區不同而產生不同結果。它在開發中能用,部署到不同區域時在生產中失效。當程式碼不帶顯式 timeZone 選項格式化日期、隨後在測試中快照該結果時,Node 中也會出現同類 bug。

  • Wrong: datetime.fromtimestamp(ts) — uses server's local timezone
  • Right: datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
  • Wrong in JS: date.toLocaleDateString() — uses the Node process timezone
  • Right in JS: date.toISOString() or Intl.DateTimeFormat with an explicit timeZone option

Bug 4:把時間戳存為字串

把時間戳作為格式化字串存入 VARCHAR 欄位看似無害,直到你需要按時間排序、查詢範圍或做算術。日期的字串比較只對嚴格的、零填充的 ISO 8601 欄位有效。像 Nov 15, 2023 或 1/2/24 這樣易讀的字串是顯示格式,而非儲存格式。

  • Wrong: INSERT INTO events (created) VALUES ('Nov 15, 2023') — not sortable
  • Wrong: INSERT INTO events (created) VALUES ('2023-11-15') — loses time component
  • Right: use a native DATETIME/TIMESTAMPTZ column or BIGINT for Unix milliseconds
  • If a string is unavoidable: use ISO 8601 with full precision: '2023-11-15T06:13:20Z'

Bug 5:解析有歧義的日期字串

new Date('2024-01-01') 和 new Date('2024/01/01') 看起來等價,行為卻不同。帶連字號的 ISO 8601 格式被解析為 UTC 午夜;帶斜線的格式由實作決定,多數瀏覽器解析為本地午夜。日期選擇器、API 負載和資料庫列可能顯示同一日曆日期卻表示不同的瞬間。

  • new Date('2024-01-01') → January 1, 2024 00:00:00 UTC (correct, explicit)
  • new Date('2024/01/01') → January 1, 2024 00:00:00 local time (varies by runtime)
  • new Date('January 1, 2024') → local time, may be rejected by strict parsers
  • Safe rule: always use ISO 8601 with explicit timezone: new Date('2024-01-01T00:00:00Z')

Bug 6:加 24 小時來表示明天

加 86,400,000 毫秒看似是取得明天的乾淨方法,但本地日曆日並不總是 24 小時。在日光節約時間切換期間,本地一天可能是 23 或 25 小時。如果產品邏輯指的是下一個本地日曆日,請在目標時區做日曆算術,而非在 UTC 毫秒中做時長算術。

  • 對日曆日是錯的:next = new Date(date.getTime() + 86400000)
  • 在本地 Date 程式碼中更好:複製 Date,然後呼叫 setDate(d.getDate() + 1)
  • 對伺服器程式碼:使用支援時區的函式庫或資料庫的日期算術
  • 對重複排程:儲存 local_date、local_time 和 timezone_id,再計算每次發生
  • 為目標時區的日光節約時間開始日與結束日加入測試

Bug 7:包含末尾的當日過濾

常見的分析查詢使用 created_at >= start 和 created_at <= endOfDay。看似合理,但當資料庫儲存毫秒、微秒或奈秒時會產生精度 bug。更安全的模式是半開區間:大於等於起點,且嚴格小於下一週期的起點。

  • Risky: WHERE created_at <= '2026-05-19 23:59:59'
  • Safer: WHERE created_at >= '2026-05-19' AND created_at < '2026-05-20'
  • Half-open ranges work for seconds, milliseconds, microseconds, and nanoseconds
  • The same pattern works for months: created_at >= monthStart AND created_at < nextMonthStart

生產環境預防清單

大多數時間戳事故都可以透過命名、測試和顯式邊界來預防。目標不是讓每一行日期程式碼都很巧妙;而是讓單位、時區和範圍語意顯而易見到乏味。

  • 用帶單位的名稱命名數值欄位:createdAtMs、expiresAtSeconds、imported_at_ms
  • 儲存用 UTC,顯示用顯式的 IANA 時區
  • 在人工檢查的 API 邊界處優先使用帶 Z 或偏移的 ISO 8601 字串
  • 日期過濾使用半開區間
  • 在 1 月 1 日、閏日、日光節約時間切換和 2038 年邊界附近做測試

FAQ

What is the most common Unix timestamp bug?
Passing seconds where milliseconds are expected, or the reverse. In JavaScript, new Date(unixSeconds) lands in 1970 because the constructor expects milliseconds — multiply the 10-digit value by 1000 first.
How do I prevent seconds-vs-milliseconds bugs?
Name fields with their unit (createdAtMs, expiresAtSeconds), convert at system boundaries, and prefer ISO 8601 strings in API contracts so the value is self-describing.
Why does adding 86,400,000 ms break 'tomorrow'?
Local calendar days are 23 or 25 hours long during daylight saving transitions, so adding a fixed 24 hours can land on the wrong date. Use calendar arithmetic in the target timezone instead.