← 全部文章

每个开发者都发布过的 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.