この記事に関連するツール
ブラウザ上ですぐに試せます。記事の内容を確認しながら使うと、作業の流れをつかみやすくなります。
「9時間足す」だけのはずが、なぜ事故るのか
UTCとJSTの関係は、実はとても単純です。JST = UTC + 9時間、固定。 日本にはサマータイムが無いので、季節によって時差が変わることもありません。ヨーロッパやアメリカのタイムゾーンを扱うときの厄介さと比べれば、計算そのものは暗算でできます。
それでもタイムゾーンの事故が絶えないのは、変換の計算ではなく「その日時が今どちらなのか分からなくなる」ことが原因だからです。ログはUTC、DBはJST、CIはUTC、開発マシンはJST——という状況で 2026-08-06 09:00:00 という文字列を見ても、それが何時なのか誰にも分かりません。
この記事は、変換の早見表と、実際に事故が起きるポイントをまとめたリファレンスです。
時差の早見表
| UTC | JST(同日) | 備考 |
|---|---|---|
| 00:00 | 09:00 | UTCの0時は日本の朝9時 |
| 03:00 | 12:00 | 日本の正午 |
| 09:00 | 18:00 | 日本の定時 |
| 12:00 | 21:00 | |
| 15:00 | 翌日 00:00 | ここから日付が変わる |
| 18:00 | 翌日 03:00 | |
| 21:00 | 翌日 06:00 |
逆方向(JST→UTC)は9時間引きます。JSTの00:00〜08:59は、UTCでは前日になります。
日付が変わる境界は UTC 15:00 です。日次バッチや集計の対象期間がずれる事故は、ほぼこの一線をまたいだところで起きます。「日本時間の1日分」を集計したいなら、UTCでは 前日15:00 〜 当日15:00 を指定することになります。
ISO 8601の Z と +09:00
日時を文字列でやり取りするときの表記を整理します。
| 表記 | 意味 |
|---|---|
2026-08-06T00:00:00Z | UTCの0時。ZはUTC(Zulu time)を表す |
2026-08-06T09:00:00+09:00 | 上とまったく同じ瞬間をJSTで書いたもの |
2026-08-06T09:00:00 | オフセット無し=どちらか不明。解釈は処理系任せ |
2026-08-06 | 日付のみ。時刻とタイムゾーンの解釈が処理系ごとに割れる |
真ん中の2つは同じ瞬間を指します。オフセット付きで書いてある限り、表記が違っても情報は失われません。
問題は3番目です。オフセットの無い日時文字列は、それ単体では意味が確定しません。 APIのレスポンスやCSVでこの形式を見かけたら、まず仕様を確認してください。
JavaScriptで日付がズレる条件
もっとも遭遇しやすい罠がこれです。同じ日付を表しているように見える2つの文字列が、別の瞬間として解釈されます。
// 日付のみの形式は UTC として解釈される
new Date('2026-08-06').toISOString();
// => '2026-08-06T00:00:00.000Z' (JSTでは 8/6 09:00)
// 時刻付きでオフセットが無い形式は「ローカルタイム」として解釈される
new Date('2026-08-06T00:00:00').toISOString();
// => '2026-08-05T15:00:00.000Z' (JST環境で実行した場合。日付が前日に)
T00:00:00 を足しただけで結果が9時間ずれ、しかも日付が変わります。この挙動はES仕様どおりで、バグではありません。日付のみの形式はUTC、時刻付きでオフセット無しはローカル、と覚えるしかない部分です。
安全な書き方は、常にオフセットを明示することです。
new Date('2026-08-06T00:00:00+09:00').toISOString();
// => '2026-08-05T15:00:00.000Z' (意図どおり。JSTの8/6 0時)
表示側でJSTに変換したいときは、toLocaleString にタイムゾーンを渡します。実行環境のタイムゾーン設定に依存しなくなるので、サーバー上でもブラウザ上でも同じ結果になります。
const d = new Date('2026-08-06T00:00:00Z');
d.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });
// => '2026/8/6 9:00:00'
// 実行環境がUTCでもJSTでも結果は変わらない
なお toISOString() は常にUTCで出力します。ローカル時刻をそのままISO文字列にしたつもりで使うと、9時間ずれた値が保存されます。
サーバーサイドでの変換
Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+
now_utc = datetime.now(timezone.utc)
now_jst = now_utc.astimezone(ZoneInfo('Asia/Tokyo'))
# naive(タイムゾーン情報なし)のdatetimeは扱わないのが鉄則
MySQL
SELECT CONVERT_TZ('2026-08-06 00:00:00', '+00:00', '+09:00');
-- => 2026-08-06 09:00:00
MySQLで注意すべきは、TIMESTAMP 型はセッションのタイムゾーンで自動変換されるのに対し、DATETIME 型は一切変換されないことです。同じテーブルに両方の型が混在していると、同じ「時刻」でも意味が違う列が並ぶことになります。名前付きタイムゾーン('Asia/Tokyo')を使うにはタイムゾーンテーブルの読み込みが必要なので、オフセット指定のほうが確実です。
PostgreSQL
SELECT TIMESTAMPTZ '2026-08-06 00:00:00+00' AT TIME ZONE 'Asia/Tokyo';
-- => 2026-08-06 09:00:00
PostgreSQLでは TIMESTAMPTZ(タイムゾーン付き)を基本にして、表示時に AT TIME ZONE で変換するのが定石です。型の使い分けは CREATE TABLE リファレンス にもまとめています。
事故が起きる4つの場所
1. cronはサーバーのタイムゾーンで動く
GitHub Actionsの schedule は常にUTCです。 「毎朝9時に実行」のつもりで 0 9 * * * と書くと、日本時間の18時に動きます。
on:
schedule:
# JSTの朝9時 = UTCの0時
- cron: '0 0 * * *'
cron式そのものの読み方は cron式の書き方、ワークフローの依存関係は GitHub Actions needs設計パターン にまとめています。
2. Dockerコンテナの既定はUTC
多くのベースイメージはタイムゾーン設定を持たず、UTCで動きます。ローカル開発(JST)とコンテナ内(UTC)でログの時刻が9時間ずれるのはこれが原因です。アプリ側で時刻を「ローカルタイム」として扱っていると、環境によって結果が変わります。
3. ログのタイムゾーンが混在する
アクセスログはサーバーのローカルタイム、アプリケーションログはUTC、監視サービスの表示はブラウザのタイムゾーン——という状態は珍しくありません。障害調査で複数のログを突き合わせるときは、まず全部をUTCかUNIX時間に揃えてから並べるのが確実です。UNIXタイムスタンプはタイムゾーンの概念を持たない絶対的な秒数なので、突き合わせの基準として使えます(詳しくは UNIX時間とは)。
4. 「日付だけ」を保存する
2026-08-06 のような日付のみの値は、タイムゾーンによって指す範囲が変わります。締め日や集計日など業務上の日付は、日時に変換せず日付のまま扱う(DATE 型を使う)ほうが安全です。日時に変換した瞬間に、どのタイムゾーンの0時なのかという問題が発生します。
鉄則: 保存はUTC、表示で変換
これらをまとめると、実務上の指針は次の3行になります。
- 保存と内部処理はUTC(またはUNIX時間)で統一する
- 表示するときだけJSTに変換する
- 文字列でやり取りするときは必ずオフセットを付ける(
Zまたは+09:00)
「日本国内向けサービスだからJSTで保存する」は一見合理的ですが、CIやコンテナがUTCで動く以上、どこかで境界が生まれます。境界は少ないほど事故が減ります。
手元で変換する
ログに出ていたUTC時刻を今すぐJSTで確認したいときは、UTC/JST変換ツール に貼り付ければ相互変換できます。ISO 8601形式にも対応していて、現在時刻の取得やコピーもその場でできます。
UNIXタイムスタンプ(1785974400 のような数値)から日時に直したいときは UNIX時間変換、タイムアウトやTTLの設定値を秒・分・時間で読み替えたいときは 時間単位変換 が使えます。いずれもブラウザ内で処理され、入力した日時が外部に送信されることはありません。
まとめ
- JST = UTC + 9時間で固定。日本にサマータイムは無い
- 日付が変わる境界は UTC 15:00。「日本時間の1日」はUTCでは前日15時〜当日15時
- ISO 8601でオフセット(
Z/+09:00)が無い文字列は、それだけでは意味が確定しない - JavaScriptは日付のみ=UTC、時刻付きオフセット無し=ローカルと解釈が割れる
- MySQLは
TIMESTAMPだけ自動変換されDATETIMEは変換されない - GitHub Actionsのcronは常にUTC。JST 9時は
0 0 * * * - 保存はUTC、表示で変換、文字列には必ずオフセット
よくある質問
JSTとUTCの時差は季節で変わりますか?
変わりません。日本標準時(JST)は UTC+9 で固定されており、サマータイム(夏時間)は導入されていません。したがって年間を通じて9時間差のままです。ヨーロッパや北米のタイムゾーンを扱う場合は夏時間の切り替えがあるため、オフセットを固定値で持たず、Asia/Tokyo のようなタイムゾーン名で扱うのが安全です。
2026-08-06T09:00:00+09:00 と 2026-08-06T00:00:00Z は違う時刻ですか?
同じ瞬間です。表記しているタイムゾーンが違うだけで、指している時点は完全に一致します。どちらの形式で保存しても情報は失われませんが、システム内では片方に統一しておくと比較やソートのときに迷いません。
JavaScriptで日付が1日ずれるのはなぜですか?
日付のみの文字列('2026-08-06')はUTCとして解釈されるのに対し、時刻付きでオフセットの無い文字列('2026-08-06T00:00:00')はローカルタイムとして解釈されるためです。JST環境では後者が9時間前(前日15:00 UTC)を指すことになり、UTC基準で日付を出力すると1日ずれて見えます。文字列には常にオフセットを付けてください。
GitHub Actionsで毎朝9時(日本時間)に実行するには?
schedule のcronはUTCで解釈されるため、- cron: '0 0 * * *' と書きます。JSTの9時はUTCの0時です。なお、GitHub Actionsのスケジュール実行は負荷状況によって数分〜十数分遅延することがあるため、分単位の正確さが必要な処理には向きません。
変換したい日時を貼り付けると、データは送信されますか?
いいえ。UTC/JST変換ツール と UNIX時間変換 はどちらもブラウザ内で処理が完結し、入力した日時がサーバーへ送信されることはありません。
おすすめリソース
このセクションにはアフィリエイトリンクが含まれる場合があります。リンク経由で購入すると、追加費用なしでDevToolKits.appが紹介料を受け取ることがあります。