タイムスタンプとISO 8601の説明
タイムゾーン災害なしにシステム間で日付と時刻を扱う方法。
本番環境のあらゆるタイムゾーンバグは、少数の概念的な誤りのいずれかに遡ります。瞬間(インスタント)と壁時計時刻の混同、オフセットとタイムゾーンの混同、あるいはパーサーが意図を推測してくれるという過信。Unixタイムスタンプと ISO 8601 というフォーマット自体は簡単な部分です。与えられたデータにどの表現が必要かを知ることこそが本当のスキルです。
インスタント vs 壁時計時刻
インスタントはグローバルなタイムライン上の一点です。「決済が承認された瞬間」。壁時計時刻はローカルの時計が表示するものです。「会議はソウル時間の09:00」。これらは異なるデータ型です。インスタントはUTCとして永遠に安全に保存できます。未来の壁時計イベントはそうではありません。後述しますが、これが最も頻繁に出荷されるタイムゾーンバグです。
Unix時間
1745939938 秒 (古典的なUnix)
1745939938123 ミリ秒 (JavaScript Date.now())
1745939938123456 マイクロ秒 (多くのデータベース)
Unix時間は1970-01-01T00:00:00Zからの秒数を数え、うるう秒を無視します。1日は定義上ちょうど86,400秒であり、うるう秒はある秒を2倍の長さにする(あるいはGoogleやAWSの時計のように数時間にわたってスミアする)ことで吸収されます。タイムゾーンがなく、コンパクトで、ソートが自明です。リスクは単位の混同です。秒の値をミリ秒として読むと1970年1月に落ち、ミリ秒の値を秒として読むと西暦57,000年に落ちます。そしてログでの可読性のなさ。タイムスタンプフィールドがどちらの単位も運べるなら、それはバグ工場です。1つの単位に標準化し、フィールド名に明示しましょう(created_at_ms)。
ISO 8601とRFC 3339
2026-04-30T15:32:18Z UTCインスタント
2026-04-30T15:32:18.123+09:00 ミリ秒とオフセット付き
2026-04-30 時刻なしのカレンダー日付
ISO 8601は期間(P3Y6M4D)、区間、週日付(2026-W18-4)、通算日まで扱う大きな標準です。APIがほぼ常に意味しているのはインターネットプロファイルの RFC 3339 です。完全な日付と時刻、ハイフンとコロンの拡張記法、そして必須のオフセットまたは Z。仕様の有用な詳細を2つ: 小文字の t や z も合法ですが慣例的に避けられ、-00:00 は歴史的に「オフセット不明」の合図でした。出力時は Z と大文字区切りに正規化しましょう。
RFC 3339文字列の決定的な性質: (単一オフセット内では)時系列順が辞書順と一致します。文字列でソートされたログは時間でソートされたログです。
オフセットはタイムゾーンではない
+09:00 はオフセット、つまり固定された数値です。Asia/Tokyo はタイムゾーンです。壁時計時刻をオフセットにマッピングするIANA tzデータベースの政治的ルールの集合であり、すべての歴史的・予定されたDST遷移を含みます。政府が急にルールを変えるため、tzデータベースは年に数回更新されます。
これが、未来のイベントにオフセットではなくタイムゾーン名が必要な理由です。「2027-03-10T09:00+01:00」ではなく「2027-03-10 09:00 Europe/Berlin」を保存してください。それまでにドイツがDSTを廃止すれば、保存されたオフセットが会議を静かに間違った時刻に固定します。対照的に、過去のインスタントは素のUTCが最も安全です。
コードで備える価値のあるDSTエッジケース
- スキップされる時刻: 春の時計進めの間、ローカルの02:30は単に存在しません。スケジューラーはポリシーを定義する必要があります(前方シフトが一般的)。
- 繰り返される時刻: 秋の時計戻しでは01:30が2回発生します。どちらかを選ぶにはオフセットか曖昧さ解消フラグが必要です。
- 日付演算: 「1日加算」と「24時間加算」は遷移日には異なります。カレンダー演算はイベントのタイムゾーンで行い、生のエポック上では決して行わないこと。
実用的な保存ルール
- データベースのインスタント: Postgresの
timestamptz(注意: UTCで保存し出入り時に変換するだけで、元のゾーンは記憶しません)、またはエポックミリ秒のBIGINT。 - APIとJSON: 明示的な
Zかオフセット付きのRFC 3339文字列。 - 未来の予定イベント: 壁時計時刻 + IANAゾーン名、実行時にのみUTCへ具体化。
- 誕生日などの日付専用値: 素の
YYYY-MM-DD、決してどこかのゾーンの真夜中にしないこと。時刻を付けた瞬間、グリニッジ以西のユーザーの誕生日が1日ずれます。
JavaScript特有の罠
Date のパースには仕様が命じる非一貫性があります。new Date('2026-04-30') はUTC真夜中と解釈される一方、時刻はあるがオフセットのない new Date('2026-04-30T09:00') はローカル時刻と解釈されます。明示的なオフセットを付ければ両方の問題が消えます。表示はプラットフォームにロケール処理を任せましょう。
new Intl.DateTimeFormat('ja-JP', {
dateStyle: 'long',
timeStyle: 'short',
timeZone: 'Asia/Tokyo'
}).format(new Date(1745939938123));
Temporal API(Temporal.Instant、Temporal.ZonedDateTime、Temporal.PlainDate)は、インスタント/壁時計/日付専用の区別をついに型システムにエンコードします。ランタイムが搭載し次第、新しいコードの正しいターゲットであり、今日の実用的な代役はdate-fnsやLuxonです。
経過時間の計測: 間違った時計と正しい時計
壁時計のタイムスタンプは何かがいつ起きたかを記録するためのもので、どれだけかかったかを測るためのものではありません。システムクロックはNTP補正中に後退し、手動変更でジャンプするため、本番トレースでは Date.now() の差分が時折負になります。あらゆるプラットフォームが区間計測用の単調クロックを提供しています。JavaScriptの performance.now()、Pythonの time.monotonic()、POSIXの CLOCK_MONOTONIC。同じ理屈はマシン間にも当てはまります。クライアントの時計は秒から分の単位でドリフトするため、レイテンシ計算のためにクライアント生成のタイムスタンプとサーバー生成のものを比較してはいけません。両方を同じ時計から発行するか、単一の観測点からラウンドトリップを測りましょう。
sdk.is/timestamp-converter のタイムスタンプコンバーターで、エポック、ISO文字列、人間可読形式の間を変換しましょう。