本番環境の Unix タイムスタンプ:秒 vs ミリ秒、2038 年問題、タイムゾーンバグ
あらゆる本番システムは、いつか Unix タイムスタンプをログに記録し、保存し、送信します。そしてあらゆる本番システムは、いつかそれに関するバグを出荷します——1970 年 1 月と表示される日付、49 年前にスケジュールされたイベント、片側がミリ秒を期待しもう片側が秒を送る API 連携。これらのバグはありふれていて退屈で、タイムスタンプの実際の動作を支配する一握りのルールを知っていれば完全に防げます。
このガイドでは実践的な核心をカバーします:桁数によるタイムスタンプ単位の識別、2038 年問題とその今日の実際の状況、実際に発生するバグパターン、そしてあなたが使っているであろう言語でタイムスタンプを取得する正しい方法。
Unix タイムスタンプとは何か(そして何でないか)
Unix タイムスタンプは 1970-01-01 00:00:00 UTC ——「Unix エポック」——からの秒数を数えます。後のすべてに関わる性質が 2 つあります:
- タイムゾーンを持たない。 タイムスタンプ
1785235837は、地球上のあらゆる場所で同時に、1 つの特定の瞬間を指します。タイムゾーンが登場するのは、タイムスタンプを人間に表示するとき、あるいはローカルの壁時計の要素からタイムスタンプを構築するときだけです。この記事から 1 文だけ心に刻むなら、これにしてください:タイムスタンプは UTC の瞬間であり、タイムゾーンは表示上の関心事です。 - うるう秒を無視する。 Unix 時間はすべての日が正確に 86,400 秒であると仮定します。うるう秒が挿入されると、POSIX 時間は実質的にそれを繰り返すか、ならします(動作はシステムによって異なり、Google の「リープスミア」は 1 日かけて分散させます)。ほとんどのアプリケーションコードでは無関係ですが、うるう秒をまたぐ間隔計算では、「正確に N 秒」という仮定が 1 秒ずれる可能性があります。
また、エポックの選択により 負のタイムスタンプも有効 であることに注意してください:-1 は 1969-12-31 23:59:59 UTC です。負のタイムスタンプをエラーとして扱うコードは 1970 年以前の日付で壊れます——歴史的データ(生年月日、アーカイブ記録)を扱うシステムでの実際のバグパターンです。
秒、ミリ秒、マイクロ秒:桁数によるヒューリスティック
タイムスタンプはエコシステムによって異なる単位で到着し、単位の誤認は最も一般的なタイムスタンプバグです。素早い現場テストは桁数を数えることです:
| 単位 | 例(2026 年半ば) | 桁数 | この桁数になる時期 |
|---|---|---|---|
| 秒 | 1785235837 | 10 | 2001-09-09 以降 10 桁、2286 年から 11 桁 |
| ミリ秒 | 1785235837000 | 13 | 2001-09-09 以降 13 桁 |
| マイクロ秒 | 1785235837000000 | 16 | 16 桁 |
| ナノ秒 | 1785235837000000000 | 19 | 2262 年に int64 の範囲を超過 |
現代(2001〜2286 年)の経験則:
- 10 桁 → 秒。 常に仮定して安全。
- 13 桁 → ミリ秒。 JavaScript の
Date.now()、Java のSystem.currentTimeMillis()、ほとんどのロギング/メトリクスシステムがこれを出力します。 - 16 桁 → マイクロ秒。 Python の
time.time()を整数化したもの、一部のデータベース、一部のトレーシングシステム。 - 19 桁 → ナノ秒。 Go の
time.Now().UnixNano()。これは罠です:19 桁は 2262 年以降は符号付き 64 ビットの最大値(9,223,372,036,854,775,807)を超えますが、今日の 19 桁の数値は int64 に収まります——ぎりぎりですが。ナノ秒タイムスタンプを保存するなら、余裕を仮定した演算はしないでください。
デバッグ時の素早い健全性チェック:タイムスタンプ変換ツール に数値を貼り付けて、日付が妥当に見えるか確認します。「秒」解釈で 1970 年になり、「ミリ秒」解釈で妥当な最近の日付になるなら、2 秒で単位を特定できました。
対応するバグパターンは ミリ秒を秒として扱う(またはその逆)ことです。秒のタイムスタンプを不必要に 1000 倍すると、1785235837 は 1785235837000 秒になり——約 56,000 年未来の日付になり、ほとんどのレンダラーはクランプするかラップしてゴミを表示します。すでに秒だったミリ秒タイムスタンプを 1000 で割ると 1970-01-21 になります——だから多くの壊れたシステムが 1970 年 1 月の日付を表示するのです。本番で 1970 年を見たら、まず単位の不一致を疑ってください。
2038 年問題
古典的な Unix の time_t は秒を数える 符号付き 32 ビット整数 でした。その最大値 2,147,483,647 は 2038-01-19 03:14:07 UTC に対応します。その 1 秒後、符号付き 32 ビットカウンターは −2,147,483,648 にラップし、1901-12-13 20:45:52 UTC と表示されます。これが「Y2038」または「エポカリプス」問題です——構造的には Y2K と同じですが、2 桁の年フィールドではなく特定の C 型に由来します。
2026 年時点で実際にまだ問題になる場所:
- 32 ビット組み込みシステムとファームウェア。 長い耐用年数を持つデバイス——産業コントローラー、自動車システム、医療機器、インフラ——で、32 ビットカーネルや 32 ビット
time_tABI で動作しているもの。これが主な残留リスク分野です。まさにこれらのシステムはパッチが難しく、忘れられやすいからです。 - 32 ビットの秒カウンターを指定した古いファイルフォーマットとプロトコル。 フィールドを符号なしとして再解釈することで修正されるものもあり(限界が 2106 年に延びる)、単にレガシーのままのものもあります。
- エポック制限のある型を持つデータベース。 よく知られた例:MySQL の
TIMESTAMP型は歴史的に2038-01-19 03:14:07 UTCまでの範囲しか持たず、秒を符号なし 32 ビット整数で保存していたためです(DATETIMEは 9999 年までの範囲があり、影響を受けません)。スキーマでTIMESTAMPを使っているなら、この制限を知っておいてください。
すでに修正されたもの:
- 64 ビット Linux と現代の 64 ビットシステム は 64 ビットの
time_tを使い、約 2,920 億年有効です。 - Linux カーネルの
time64作業(カーネル 5.6、2020 年で完了)と glibc の 64 ビット時間サポート(glibc 2.34 の_TIME_BITS=64)により、32 ビット Linux ユーザー空間に移行パスができました。ただしバイナリは新しい ABI で再ビルドが必要です——32 ビットシステム上の古いコンパイル済みバイナリは脆弱なままです。 - JavaScript、Python、Go、Java はすべて 64 ビット(または float64)の時間表現を使い、人間に関係する時間枠ではオーバーフローしません。JavaScript の float64 ミリ秒は両方向に約 285,000 年いけます。
実践的なアドバイス:64 ビットプラットフォーム上で主流の言語で新しいコードを書いているなら、2038 年はあなたの問題ではありません。組み込みファームウェア、32 ビットターゲットのレガシー C、INT のエポックカラムや MySQL の TIMESTAMP フィールドだらけのスキーマを保守しているなら、今すぐ監査を——2020 年代に設置されたシステムは 2038 年にもまだ稼働しているでしょう。
実際に発生するバグパターン
1. 単位の混同(秒 vs ミリ秒)
上でカバーしました:最も一般的で、最も見つけやすいものです(1970 年か 58000 年の日付)。規約で修正:変数とフィールドに単位を付けた名前を——created_at_ms、expires_s——付け、API 境界で検証します。この世紀の「秒」フィールドとしては、たとえば 10^9〜10^10 秒の範囲外のタイムスタンプは疑わしいものです。
2. ローカル時刻を UTC と誤認
「タイムスタンプにタイムゾーンはない」ルールの逆です。典型的な現れ方:上海のサーバーとバージニアのサーバーが「同じ」締切を異なって計算するのは、片側がローカルの壁時計要素からタイムスタンプを構築したからです。ローカル日時フィールド → タイムスタンプ の経路を通るコードは、それらのフィールドのタイムゾーンを明示的に指定しなければなりません。JavaScript の new Date(2026, 6, 28) は ブラウザのローカルタイムゾーン の真夜中を意味し、ユーザーごとに異なる瞬間になります。
3. 夏時間のエッジケース
DST(夏時間)は 2 種類の壊れた壁時計時刻を生み出します:
- 存在しない時刻。 米国のタイムゾーンでは、2026-03-08 02:30(ローカル)は存在しません——時計は 02:00 → 03:00 に跳びます。「毎日 02:30 に実行」に設定された cron 風スケジューラーは、その日はスキップするか、二重実行するか、プラットフォーム依存の動作をします。
- 曖昧な時刻。 2026-11-01 には、米国のタイムゾーンで時計が戻されるため、01:30(ローカル)が 2 回 発生します。オフセットやタイムゾーンなしで「01:30」を保存しても、どちらを意味したか区別できません。
堅牢な修正:繰り返しジョブは UTC でスケジュールし、瞬間はタイムスタンプで保存し、ローカル時刻への変換は表示時のみ行うこと。未来のローカル壁時計時刻(例:「ユーザーの午前 9 時の会議」)を保存しなければならない場合は、ローカル時刻 プラス IANA タイムゾーン名(America/New_York)を保存し、レンダリング時にタイムスタンプへ解決します——政府は DST ルールを変更するため、2028 年のローカル会議のために今日計算したタイムスタンプは、法律変更後には間違っているかもしれないからです。
4. JavaScript のゼロ起点の月
new Date(2026, 6, 28) は 6 月 28 日ではなく 7 月 28 日です——月は 0〜11 で、日は 1〜31 で動きます。この不整合は一世代分の 1 か月ずれバグを生み出してきました。緩和策:UTC を意味する場合は Date.UTC(...) を使う、明確さのために ISO 文字列(new Date("2026-07-28T00:00:00Z"))を使う、またはライブラリを使う(Temporal が広く利用可能になれば、1 起点の月でこれを適切に修正します)。
5. 秒の浮動小数点精度
Python の time.time() は float を返します。float64 は 53 ビットの仮数部を持ち、現在のエポックの大きさ(約 1.79 × 10^9 秒)では、分解能は十分です(約 240 ナノ秒)。そのため Python でこれが問題になることは稀です——しかし同じ理屈は、マイクロ秒やナノ秒のタイムスタンプを float64 で保持する システムでは問題になります:16 桁のマイクロ秒値は正確に表現するのに 54 ビット必要なため、float64 のマイクロ秒は今日すでに不正確です。ミリ秒より細かいものには整数を使ってください。
6. オフセットなしの文字列パース
JavaScript で new Date("2026-07-28") は UTC の真夜中としてパースされますが、new Date("2026-07-28 00:00:00")(T なし、Z なし)は ローカル の真夜中としてパースされます——文字列フォーマットの細部に基づく静かなタイムゾーン反転です。シリアライズされた日時には常に明示的な Z または数値オフセットを含めてください。オフセット付き ISO 8601 を推奨:2026-07-28T10:00:00+08:00。
言語ごとの正しいタイムスタンプ取得
JavaScript / TypeScript — Date.now() は ミリ秒 を返します。秒には割ります:
const ms = Date.now(); // 1785235837123
const seconds = Math.floor(Date.now() / 1000); // 1785235837
// From a specific instant, always specify UTC explicitly:
const ts = Date.UTC(2026, 6, 28, 0, 0, 0) / 1000; // months 0-indexed!
Python — time.time() は float の秒を返します。保存するものには明示的な UTC で datetime を使います:
import time
from datetime import datetime, timezone
seconds = int(time.time()) # 1785235837
ns = time.time_ns() # nanoseconds, exact integer
ts = int(datetime.now(timezone.utc).timestamp())
Go — time パッケージが各単位を明示的に提供します:
now := time.Now()
seconds := now.Unix() // int64 seconds
millis := now.UnixMilli() // int64 milliseconds
nanos := now.UnixNano() // int64 nanoseconds — mind int64 range
Java — レガシーな Date/Calendar は避け、java.time を使います:
long millis = System.currentTimeMillis();
long seconds = Instant.now().getEpochSecond();
long nanos = Instant.now().getNano(); // nano-of-second, not epoch nanos!
Bash — GNU と macOS(BSD)の date はパースが異なります:
date +%s # current epoch seconds (both)
date -d @1785235837 # GNU: timestamp → human date
date -r 1785235837 # macOS/BSD: timestamp → human date
date -u -d "2026-07-28 00:00:00 UTC" +%s # GNU: date → timestamp
date -u -j -f "%Y-%m-%d %H:%M:%S" "2026-07-28 00:00:00" +%s # macOS
繰り返し現れるテーマに注目してください:正しい API はすべて、エポック値を直接返すか、タイムゾーンの指定を強制します。危険な API はすべて、それを省略させます。
タイムスタンプを対話的に扱う
ログや API レスポンスのタイムスタンプをデバッグするとき、頭の中で計算しないでください。当サイトの タイムスタンプ変換ツール に貼り付けましょう:UTC とあなたのローカルタイムゾーンの両方でその瞬間を表示し、秒 vs ミリ秒を自動検出します——まさに上で説明した曖昧さチェックです。逆方向(人間の日付 + タイムゾーン → タイムスタンプ)の変換もでき、テストフィクスチャの構築に使えます。
まとめ
- Unix タイムスタンプは 1970-01-01 UTC からの秒(または ms/µs/ns)です。タイムゾーンもうるう秒もありません。
- 桁数で現代の単位を識別できます:10 = 秒、13 = ミリ秒、16 = マイクロ秒、19 = ナノ秒。
- 2038 年に符号付き 32 ビット
time_tが 2038-01-19 03:14:07 UTC でオーバーフローします。64 ビットシステムと主流言語は安全。組み込みファームウェア、レガシー 32 ビットバイナリ、MySQL のTIMESTAMPカラムが残留リスクです。 - 現実世界のバグパターン:単位の混同、ローカル vs UTC の間違い、DST のエッジ、JS のゼロ起点の月、µs/ns での float64 精度損失、オフセットなしの文字列パース。
- 瞬間は整数タイムスタンプで保存し、フィールドには単位を付けた名前を付け、UTC でスケジュールし、ローカルへの変換は表示時のみ——そして、それでもすり抜けるデバッグのために タイムスタンプ変換ツール をブックマークしておきましょう。