Unix 時間が実際に数えているもの(秒・ミリ秒・ナノ秒)
Unix 時間はただ 1 つの整数、すなわち紀元(epoch)である 1970-01-01T00:00:00Z からの秒数です。この日付自体は恣意的なものです — ベル研究所の初期 Unix には、妥当な分解能で 32 ビット値に収まるゼロ点が必要で、1970 年の始まりは手近で切りのよい数字でした。負の値も完全に合法で、1970 年以前の瞬間を表します。-86400 は 1969 年の最終日です。カウントは UTC に固定されているため、ソウルとサンパウロの 2 台のマシンが同じ瞬間にタイムスタンプを生成すれば同じ数値になります。タイムスタンプは瞬間を識別するのであって、壁時計の読みを識別するのではありません。
ここでの古典的なバグ一族は単位の混同です。エコシステムごとに分解能が違うからです。POSIX の time()、ほとんどのデータベース、JWT のクレームは秒を使い、JavaScript の Date.now() と Java の System.currentTimeMillis() はミリ秒を返し、多くの API と DB はマイクロ秒を、Go の UnixNano や可観測性の世界の大半はナノ秒を使います。混ぜると日付は 1970 年に落ちるか(ミリ秒を秒として扱い、ほぼすべてを割り捨てる)、西暦 56,000 年の彼方へ飛びます(秒をミリ秒として扱う)。
現代の値には桁数が最速の診断法です。10 桁は秒(1751414400 は秒として 2025 年 7 月)、13 桁はミリ秒、16 桁はマイクロ秒、19 桁はナノ秒。関連する罠が浮動小数点の精度です。タイムスタンプを JSON の浮動小数点として渡すと、秒単位なら無事ですがナノ秒の値は壊れます。64 ビット double の整数精度は 53 ビットしかないからです — 高分解能の API がタイムスタンプを文字列で渡す理由のひとつです。
Same instant, four resolutions:
1751414400 seconds (10 digits)
1751414400000 milliseconds (13 digits) Date.now()
1751414400000000 microseconds (16 digits)
1751414400000000000 nanoseconds (19 digits) Go UnixNano
// The two classic unit bugs (JavaScript):
new Date(1751414400) // 1970-01-21 — seconds fed as ms
new Date(1751414400 * 1000) // 2025-07-02 — correctうるう秒 — すべてのタイムスタンプに潜む嘘
Unix 時間は 1970 年からの秒数を数えると称していますが、文字通りには真実ではありません。地球の自転は不規則なので、UTC は天文時刻との整合を保つため、ときどきうるう秒を挿入します(1972 年以降 27 回)。Unix 時間はこれが存在しないふりをします。定義上、すべての日はちょうど 86,400 秒です。うるう秒の挿入中、UTC の時計は 23:59:60 を指しますが、Unix のカウンタはその瞬間を表現できません — 実装は 1 秒を繰り返すか停止します。つまり現実の異なる 2 つの瞬間が 1 つのタイムスタンプを共有しえて、うるう秒をまたいで測った間隔は 1 秒ずれます。
これは机上の話ではありません。2012 年 6 月のうるう秒は、カーネルのタイマーコードの livelock を介して業界中の Linux システムをクラッシュさせ、航空会社の予約システムなどを道連れにしました。2016 年末のうるう秒は Cloudflare で本番障害を起こしました。タイムスタンプの引き算で負の経過時間が出て、コードがそれを処理できなかったのです。教訓は一般化できます: 後で取った壁時計のタイムスタンプの方が大きいとは決して仮定しないこと。経過時間の計測には単調時計(monotonic clock)を使いましょう — JS の performance.now()、POSIX の CLOCK_MONOTONIC、Python の time.monotonic()。うるう秒にも NTP のステップにも手動の時刻変更にも免疫があります。
大手事業者はうるう秒スメアリングで問題を迂回します — Google と AWS は 24 時間の窓で各秒をわずかに引き延ばし、うるう秒を顕微鏡レベルの減速の中へ消し去ります。そして終着点は廃止です。2022 年の国際度量衡総会は、2035 年までにうるう秒の挿入をやめることを決議しました。それまでの間、タイムスタンプは瞬間の優れた識別子であり続けますが、経過時間の物差しとしては微妙に信頼できません。
2038 年問題はもう始まっている
符号付き 32 ビット整数の上限は 2,147,483,647。Unix タイムスタンプとしては 2038-01-19T03:14:07Z で、その 1 秒後に値は負へラップし、脆弱なシステムは突然、今が 1901-12-13 だと信じ込みます。12 年先と聞くと先の話のようですが — 未来の日付を計算するコードは何十年も早くこの境界を越えます。20 年の住宅ローン返済表は 2018 年に越えました。15 年の証明書有効期限や長い TTL は今日越えています。システムは期日の何年も前から、すでに本物の 2038 バグを生み出してきました。
解決策である 64 ビット time_t は新しいプラットフォームでは何年も前から標準で、64 ビットの秒カウンタは約 2,920 億年安全です。残滓は周縁に生きています。2038 年にもまだ稼働しているであろう 32 ビット ABI の組み込み・IoT 機器、32 ビットの時刻フィールドを持つ古いファイル形式やネットワークプロトコル、そしてデータベース。MySQL の TIMESTAMP カラム型は有名な生き残りです。32 ビットのエポック秒を格納するため、歴史的に 2038-01-19 以降を表現できません(制限のない DATETIME を使いましょう)。Linux カーネルはバージョン 5.6(2020 年)で 32 ビットアーキテクチャ向けの 64 ビット時刻システムコールを追加し、glibc は _TIME_BITS=64 ビルドオプションで追随。ext4 はタイムスタンプを 2446 年まで、XFS は 2486 年まで拡張しました。
隣接するオーバーフローも 2 つ知っておく価値があります。同じ 32 ビットを符号なしとして再解釈して 2038 年を「修正」したシステムは、2106-02-07 までの時間を稼いだだけです — 先送りであって解決ではありません。ミリ秒の世界には独自の崖があります。組み込みの稼働時間クロックによくある 32 ビットのミリ秒カウンタは 49.7 日ごとにラップします。Windows 95 で有名になり、後にはボーイング 787 の発電機制御に再起動を義務付ける耐空性指令まで生んだバグパターンです。
2^31 - 1 = 2147483647 → 2038-01-19T03:14:07Z (signed 32-bit)
+1 second → -2147483648 = 1901-12-13T20:45:52Z
2^32 - 1 = 4294967295 → 2106-02-07T06:28:15Z (unsigned 32-bit)
2^63 - 1 (64-bit) → ~292 billion years (safe)
-- MySQL: TIMESTAMP is 32-bit, DATETIME is not
CREATE TABLE t (
ts TIMESTAMP, -- max 2038-01-19 03:14:07 UTC
dt DATETIME -- max 9999-12-31 23:59:59
);タイムゾーンと夏時間 — タイムスタンプのバグが生まれる場所
Unix タイムスタンプは絶対的な瞬間であり、タイムゾーンは表示のルールです。この 2 つの役割を分けておくことが、日付バグに対する唯一最強の防御です。保存と送信は UTC タイムスタンプで行い、ローカル時刻への変換は表示の際にだけ行うこと。「タイムスタンプ」がローカル時刻の解釈をこっそり経由した瞬間 — Python の naive datetime、サーバーローカル時刻で埋められた DATETIME カラム、datetime-local のフォーム入力 — 同じ値がマシンごとに違う瞬間を意味するようになります。
夏時間(DST)は、単一のタイムゾーン内でさえローカル時刻が危険である理由です。時計が春に進むと、ローカル時刻の 1 時間がそもそも存在しなくなります(米東部では 3 月の切替日の午前 2:30 は無効)。秋に戻ると 1 時間が 2 回発生し、どちらの通過か分からなければ「午前 1:30」は曖昧です。スケジューリングのコードは両方に当たります。毎日 02:30 のジョブは年に 2 回、発火しないか二重に発火し、素朴な「24 時間を足す」計算は切替日にずれます。その日は 23 時間か 25 時間しかない(ある)からです。
さらに 2 つのルールがあります。第一に、オフセットはタイムゾーンではありません。+09:00 はある瞬間の描き方を教えるだけで、Asia/Seoul や America/Sao_Paulo のような IANA ゾーン名だけが DST と政治の歴史全体を担います — そしてその歴史は変わります。tzdata パッケージが年に数回更新されるのはそのためです(ブラジルは 2019 年に DST を廃止、エジプトは 2023 年に再導入)。第二に、言語のパースの癖を知ること。JavaScript では new Date('2026-07-02') は UTC の真夜中として、new Date('2026-07-02T00:00') はローカル時刻としてパースされます — グリニッジの東でも西でも、あらゆるユーザーに古典的な「1 日ずれ」バグを生む、たった 1 文字の違いです。
// JavaScript parsing quirk (machine in UTC+9):
new Date('2026-07-02') // date-only → parsed as UTC
.toString() // Thu Jul 02 2026 09:00 GMT+0900
new Date('2026-07-02T00:00') // date-time → parsed as LOCAL
.toString() // Thu Jul 02 2026 00:00 GMT+0900
// Same string minus one 'T00:00' = 9 hours apart.
// DST gap: 2:30 AM does not exist on spring-forward day
// America/New_York, 2026-03-08: 01:59:59 → 03:00:00ISO 8601 と RFC 3339 — 人間とソートのためのタイムスタンプ
タイムスタンプが読める必要があるときの答えは ISO 8601 系です: 2026-07-02T09:30:00+09:00。実務で API が実装しているのは RFC 3339 で、インターネット用に意図的に小さく絞った ISO 8601 のプロファイルです。ISO 標準全体には API では決して見かけないものが許されています — 2026-W27-4 のような週日付、2026-183 のような通算日、切り詰め形式、ダッシュなしの基本形式。RFC 3339 はそれらをすべて切り落とします。完全な年月日、区切りの T、必須の UTC オフセット、そして +00:00 の省略形である Z。API を設計するなら「Z 付きの RFC 3339」が最も相互運用性の高い選択です。JavaScript の toISOString() も Python の datetime.isoformat() も準拠した出力を出します。
この形式のキラー機能は、辞書順がそのまま時系列順になることです — ただし、すべての値が同じオフセットと同じ桁幅を使っている場合に限ります。オフセットが混在した文字列をアルファベット順に並べるのは誤りです(+09:00 の朝が、それより後の Z 付きの午後の後ろに並びます)。すべてを Z に正規化すべきもうひとつの理由です。このソート可能性こそ、ログファイル、S3 のオブジェクトキー、バックアップ名が慣習的に UTC の ISO タイムスタンプで始まる理由です。
噛みつく細部: RFC 3339 は -00:00 を許します(歴史的に「オフセット不明」を意味し、UTC を断言する +00:00 とは異なります)。小数秒は任意かつ可変幅なので、パーサは .5 も .500 も .500000 も受け入れる必要があります。そして日付だけの 2026-07-02 はそもそもタイムスタンプではありません — 前節で見たように、パーサによって異なる真夜中が割り当てられます。システム間で瞬間をやり取りするときは、整数のエポック値か、明示的なオフセット付きの完全な RFC 3339 文字列を送ってください。それより曖昧なものは、2 つのシステムが食い違うための招待状です。