每個開發者都發布過的 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 小時可能落到錯誤的日期。請在目標時區做日曆算術。