← 記事一覧

あらゆる開発者が出荷したことのある 7 つの Unix タイムスタンプ バグ

本番のインシデントを引き起こすタイムスタンプの誤り:静かな単位の不一致、タイムゾーンの思い込み、忘れられた ×1000、文字列保存、あいまいな解析、夏時間の境界エラー。各バグに症状・根本原因・実践的な修正を添えます。

バグ 1:JavaScript で × 1000 を忘れる

new Date(1700000000) は 2023 年 11 月ではなく 1970 年 1 月付近の日付を生みます。JavaScript の Date コンストラクターはミリ秒を期待しますが、ほとんどのサーバー API とデータベースは Unix 秒を返します。このバグはまず管理ダッシュボードで現れがちです。バックエンドのペイロードは妥当な JSON で、UI が単に誤った年を表示するからです。

  • 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)

バグ 2:システム境界での単位の混在

JavaScript はミリ秒を返します。Python・Go・ほとんどのサーバー側 API は秒を返します。JavaScript が変換せずに Python バックエンドへタイムスタンプを送ると、バックエンドはミリ秒値を秒と解釈し、イベントを 55792 年に置きます。逆方向も同じく悪く、UI が秒を受け取り、そのまま 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

バグ 3:サーバーのローカルタイムゾーンに依存する

Python の datetime.fromtimestamp() を UTC を指定せずに使うコードは、サーバーに設定されたタイムゾーンによって異なる結果を生みます。開発では動き、別リージョンへデプロイすると本番で壊れます。同種のバグは、コードが明示的な timeZone オプションなしで日付を整形し、その結果をテストでスナップショットするときに Node でも現れます。

  • 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

バグ 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'

バグ 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')

バグ 6:翌日を得るために 24 時間を足す

86,400,000 ミリ秒を足すのは翌日を得るきれいな方法に見えますが、ローカルの暦日は常に 24 時間とは限りません。夏時間の切り替え時、ローカルの 1 日は 23 時間や 25 時間になりえます。プロダクトのロジックが次のローカル暦日を意味するなら、UTC ミリ秒での期間算術ではなく、対象タイムゾーンでの暦算術を使ってください。

  • 暦日には誤り:next = new Date(date.getTime() + 86400000)
  • ローカル Date コードでより良い:Date をコピーし、setDate(d.getDate() + 1) を呼ぶ
  • サーバーコード向け:タイムゾーン対応ライブラリまたはデータベースの日付算術を使う
  • 繰り返しスケジュール向け:local_date・local_time・timezone_id を保存し、各発生を計算する
  • 対象タイムゾーンの夏時間の開始日と終了日のテストを追加する

バグ 7:終端を含む 1 日フィルター

よくある分析クエリは created_at >= start と created_at <= endOfDay を使います。妥当に見えますが、データベースがミリ秒・マイクロ秒・ナノ秒を保存する場合に精度バグを生みます。より安全なパターンは半開区間です。開始以上、かつ次の期間の開始より厳密に小さい、です。

  • 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.