每個開發者都發布過的 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 年邊界附近做測試

Unix 時間戳 bug 常見問題

最常見的 Unix 時間戳 bug 是什麼?
在期望毫秒的地方傳入秒,或反過來。在 JavaScript 中,new Date(unixSeconds) 會落在 1970 年,因為建構函式期望毫秒;先把 10 位的值乘以 1000。
如何預防秒對毫秒的 bug?
用帶單位的名稱命名欄位(createdAtMs、expiresAtSeconds),在系統邊界處轉換,並在 API 契約中優先使用 ISO 8601 字串,使值能自我描述。
為什麼加 86,400,000 毫秒會破壞「明天」?
本地日曆日在日光節約時間切換期間為 23 或 25 小時,所以加固定的 24 小時可能落到錯誤的日期。請在目標時區做日曆算術。