Unix タイムスタンプ、エポック、そして 2038 年問題

Unix タイムスタンプが実際に何であるか、誰もが踏む秒 vs ミリ秒のバグ、そして符号付き 32 ビットの時刻がなぜ 2038 年 1 月 19 日 03:14:07 UTC にオーバーフローするのかを整理します。

Unix タイムスタンプは 1 つの整数です。エポックと呼ばれる固定の時点 1970-01-01 00:00:00 UTC から経過した秒の数です。UTC の固定点から秒を 数えるというその 1 つの設計判断が、タイムスタンプを保存しやすく、比較 しやすく、タイムゾーンのあいまいさから自由にします。それはまた、本番 システムで最もよくある 2 つのバグを用意します。秒とミリ秒の間の 1000 倍の ずれのバグ、そして 2038 年の正確な瞬間に符号付き 32 ビットの時計を襲う オーバーフローです。

その数字が実際に表すもの

値は秒の単調増加するカウントです。0 がエポックです。10000000002001-09-09 01:46:40 UTC です。17000000002023-11-14 22:13:20 UTC です。2026 年半ばでは、秒単位の現在のタイムスタンプは 17 で始まる 10 桁の 数字です。

カウントは保存時点でタイムゾーンに依存しません。タイムスタンプはオフセットも、 ロケールも、「これは東京時間だった」という印も持ちません。それはグローバルな タイムライン上の 1 つの瞬間で、UTC のエポックからの秒で表されるので、地球の 反対側にある 2 台のマシンが同じ瞬間に同じタイムスタンプを生みます。タイム ゾーンは、その整数を人が読める文字列に変換するときにだけ登場します。表示の 問題であって、保存の問題ではありません。

それがタイムスタンプを保存に向いたものにする点です。1 つの瞬間につきちょうど 1 つの正規の値であり、異なる出所のイベントを並べ替え・比較・差分するのに変換が 要りません。

秒 vs ミリ秒 vs マイクロ秒

最も頻繁なタイムスタンプのバグは 2038 とは何の関係もありません。単位の混同 です。プラットフォームごとに異なる解像度を使います。

  • — ほとんどの Unix システムコール、POSIX time()、Linux の date +%s コマンド、PostgreSQL の EXTRACT(EPOCH …)、ほとんどの データベースのエポック列、JWT の exp/iat/nbf クレーム。
  • ミリ秒 — JavaScript。Date.now() は秒ではなくミリ秒を返します。Java の System.currentTimeMillis() やほとんどの JVM の日付 API もそうです。
  • マイクロ秒 / ナノ秒 — 高解像度タイマー、一部のトレーシングシステム、Go の time.UnixNano()

典型的な失敗は、JavaScript のミリ秒値を秒を期待する関数に渡すか、その逆です。 あなたを間違った千年紀に落とす 1000 倍のずれです。秒の値 1700000000 をミリ秒 として読むと 1970-01-20、エポックから 20 日後です。ミリ秒の値 1700000000000 を秒として読むと 55840 年です。

たいてい桁数でどの単位かが分かります。現在の時代のどの時刻でもそうです。

  • 10 桁 → 秒 (例: 1748000000、2025 年 5 月)
  • 13 桁 → ミリ秒 (例: 1748000000000)
  • 16 桁 → マイクロ秒
  • 19 桁 → ナノ秒

この目安は 2001 年 (10 桁の秒が始まる) から 2286 年 (秒が 11 桁に達する) まで 有効です。だから現実的に扱うどのタイムスタンプでも、桁数が単位を教えてくれ ます。迷ったら両方の解釈を変換し、もっともらしい年代に落ちるほうを選んで ください。

2038 年問題

多くの古いシステムは時刻を符号付き 32 ビット整数で保存します。32 ビット プラットフォームで歴史的に定義されていた C の time_t 型です。符号付き 32 ビット整数は −2147483648 から 2147483647 までの値を保持できます。その 上限、エポックから 2^31 − 1 = 2147483647 秒は次のとおりです。

2038-01-19 03:14:07 UTC

2038 年 1 月 19 日のちょうど 03:14:08 UTC に、カウンタはオーバーフローせずに 増加できません。符号付き整数では 2147483647 を超えて増加すると −2147483648 にラップし、これは 1901-12-13 20:45:52 UTC です。これをやる時計は 2038 年 1 月から 1901 年 12 月へ飛びます。これが 2038 年問題で、ときに Y2038 や 「Epochalypse」と書かれます。

実際に壊れるものは次のとおりです。

  • 組み込み・レガシーシステム — ルータ、産業用コントローラ、POS 端末、 そしてパッチされない 32 ビットファームウェアで動く、その他の長寿命の機器。
  • 古いデータベーススキーマ — 時刻を 32 ビットに固定した列や直列化形式。 MySQL の TIMESTAMP 型はこの理由で歴史的に 2038-01-19 03:14:07 UTC が 上限でした。DATETIME にはその制限がありませんでした。
  • 未来の日付を計算するソフトウェア — 30 年ローンの返済、長期失効の証明書、 はるか未来のキャッシュ TTL。これらは 2038 年ではなく 今日 2038 の演算に ぶつかります。2038 年 1 月より後の日付を計算するコードは、32 ビットの time_t で何年も前から失敗してきました。

解は 64 ビットの time_t です。符号付き 64 ビット整数はおよそ 292277026596 年まで秒を数えます。だいたい 2920 億年後で、問題を永久に 廃止します。64 ビットのオペレーティングシステムと現代の言語はすでに 64 ビット の時刻を使い、Linux は 2020〜2021 年ごろ 32 ビットアーキテクチャのカーネルと glibc で time_t を拡張しました。移行の現実は、リスクが今や現在のコンパイラ ではなく、デプロイ済みのファームウェアや古いバイナリに宿るということです。 64 ビットプラットフォームで新たにコンパイルしたものはすでに安全で、危険なのは 誰も再コンパイルしない現場の 32 ビット機器です。

1901 年の下限と符号なしの変種

同じ 32 ビット幅が下限も作ります。符号付き 32 ビットの time_t1901-12-13 20:45:52 UTC (−2147483648 の境界) より前のどの瞬間も表現 できません。それより早い日付、19 世紀の生年月日や歴史的事件は下方向に オーバーフローします。これが一部の古いシステムが 1901 年より前の日付をまるごと 誤って扱う理由です。

いくつかのシステムは代わりに 符号なし の 32 ビット整数を使いました。 エポックより前の時刻を表現できません (負の値がない) が、上限を 2^32 − 1 = 4294967295、つまり 2106-02-07 06:28:15 UTC まで押し上げます。 すべての 1970 年より前のタイムスタンプを代償に、約 68 年を稼ぎます。一部の ネットワークプロトコルやファイル形式で今も見かけますが、一般的な解ではなく 回避策です。

タイムゾーンは表示層に宿る

タイムスタンプは定義上 UTC なので、タイムゾーンは保存された値の一部には決して なりません。それは整数を人向けにフォーマットする層に完全に属します。同じ 1748000000 は東京・ロンドン・ニューヨークで異なる壁時計の文字列に レンダリングされますが、同じ瞬間で同じ保存された数字です。

最もよくあるタイムゾーンのバグは、その逆の間違いです。ローカルの壁時計の時刻を 取り、その構成要素をすでに UTC のタイムスタンプであるかのように保存することです。 America/New_York のサーバーがローカルの「午後 3:00」を読み、「午後 3:00 UTC」の エポックからの秒を書くと、すべての消費者がオフセットの分だけずれます。冬は 5 時間、夏は 4 時間、サマータイムに応じて動きます。ルールはこうです。1 つの 瞬間を捉えた瞬間に UTC に変換し、タイムスタンプを保存し、人にレンダリングする ときにだけタイムゾーンを適用してください。

うるう秒: Unix 時間は存在しないふりをする

UTC は地球の自転と時計を合わせるために、ときどきうるう秒を挿入します。 1972 年以降 27 個が追加されました。Unix 時間はそれらを無視します。定義上、Unix タイムスタンプはすべての日がちょうど 86400 秒だと仮定します。うるう秒が発生 しても Unix 時間は新しい値を得ません。カウントが 1 日 86400 のモデルと整合する ように、時計は通常 1 秒を繰り返すか、ならし (smear) ます。

実用上の含意は次のとおりです。

  • Unix タイムスタンプは 1970 年以降の物理的な SI 秒の真のカウントでは ありません。挿入されたうるう秒の数だけずれています。
  • うるう秒をまたぐ 2 つのタイムスタンプの間の正確な期間の計算は、実際の ストップウォッチに対して 1 秒ずれます。
  • うるう秒の間、2 つの異なる実際の瞬間が同じ Unix タイムスタンプに写像され うるので、タイムスタンプは秒未満の境界で厳密に一意であることは保証されません。

ほとんどすべてのアプリケーションコードではこれは問題になりません。高精度な タイミング、取引の順序付け、科学的な作業では問題になり、そのときは TAI や うるう秒をならす (leap-smearing) 時計ソースに手を伸ばします。

ISO 8601 vs 生のタイムスタンプ

Unix タイムスタンプは機械には理想的で、人には役立ちません。1748000000 は 一目では何も教えてくれません。ISO 8601 は人が読める交換形式です。 2025-05-23T11:33:20Z で、末尾の Z は UTC (「Zulu」) を意味するか、 2025-05-23T07:33:20-04:00 のように明示的なオフセットを伴います。

どちらをいつ使うかは次のとおりです。

  • Unix タイムスタンプを保存 — 値が内部用で、演算や比較のために最も小さく 速い表現が欲しいとき。
  • ISO 8601 を保存または送信 — 人や異種のシステムが値を読むとき、明示的な オフセットが必要なとき、または誰かが目で見るログ・API ペイロード・設定 ファイルに値が入るとき。おまけに ISO 8601 は UTC で辞書順に並びます。

妥当なデフォルトはこうです。タイムスタンプを内部的に保存し、API の境界で ISO 8601 に直列化してください。

両者の変換

ログ行で 10 桁や 13 桁の数字をにらみながらその瞬間を知る必要があるとき、または 与えられた日付のタイムスタンプを作る必要があるとき、私たちの タイムスタンプ変換器 は双方向を処理し、秒か ミリ秒かを自動検出し、結果を UTC とローカルのタイムゾーンで並べて表示します。 REPL を開かずに「1000 倍ずれたのかタイムゾーンがずれたのか」という問題を最も 速く解決する方法です。