集計結果の日付が1日ズレていることに気づき、画面のカレンダーと数字を見比べている一人運用の保守担当者

日付・タイムゾーンのバグ調査|「1日ズレる」の原因を追う手順

「昨日の売上を集計したら、1日分ズレている」 「月初の件数だけ、どうしても合わない」 「利用者から『登録日が前の日になっている』と問い合わせが来た」

——こういう報告が来たとき、いちばん困るのは、コードを読んでも、どこも間違って見えないことだと思います。

計算式は合っている。テストでも再現しない。自分の画面で試すと正しく出る。なのに、本番の数字だけがズレている。そして「たぶんデータの入り方の問題です」とも言い切れず、報告も止まる。

その手前で立ち止まっているなら、疑う先はひとつ絞れます。日付とタイムゾーンです。ここのバグは、ロジックの誤りではなく「同じ時刻を、どこが何時だと思っているか」の食い違いとして起きます。だから読んでも見えません。見えないだけで、確かめる順番はちゃんとあります。

結論:調査はこの順番でやります。①ズレの「幅」を測る(9時間か・1日か・一部だけか)→ ②生のデータを見る(画面の表示ではなく、DBに入っている値そのもの)→ ③4つの層のタイムゾーン設定を突き合わせる(サーバー・DB・アプリ・画面)→ ④ズレる時間帯で再現する → ⑤直す場所をひとつに決める。いちばんの勘所は、幅を先に測ること。9時間ちょうどなら設定の食い違い、ぴったり1日なら日付の境界、バラバラなら別の原因です。幅が分かった時点で、疑う場所は3分の1に減ります。

なお、タイムゾーンの扱いは言語・DB・フレームワーク・ホスティング環境(自社サーバー/レンタルサーバー/クラウド/コンテナ)でかなり違います。本文は一般的な進め方の一例です。コマンドや設定は本番で実行する前に、検証環境と公式ドキュメントで挙動を確認してください。

何が起きているか:日付は4つの層を通っている

ひとつの日付が「画面」「アプリ」「データベース」「サーバー」の4つの層を通り、各層がそれぞれ自分のタイムゾーンを持っていることを示した図
日付は4つの層を通ります。どの層も自分の時計を持っていて、ひとつでも違うとズレが生まれます

ひとつの「2026年8月22日 2時12分」という時刻は、画面に出るまでに何度も持ち替えられています。

  1. サーバー(OS):cronやログの時刻を決めている、いちばん下の時計。
  2. データベース:接続ごとに「いまは何時か」を持っています。ここが UTC のままの現場は珍しくありません。
  3. アプリケーション:PHPやPython、Node.jsなどが持つ既定のタイムゾーン。
  4. 画面(ブラウザ・スマホ):利用者の端末が持っている時計。

厄介なのは、この4つは別々に設定できてしまうことです。サーバーは Asia/Tokyo、DBは UTC、アプリは設定漏れで UTC——という組み合わせが、ごく普通に成立します。しかも、それぞれの層は「自分の時計が正しい」と信じて動くので、どこもエラーを出しません。静かにズレるだけです。

そしてもうひとつ。日本標準時(JST)は世界標準時(UTC)より9時間進んでいます。日本にはサマータイムがないので、この差は年間を通じて9時間で固定です。この「9」という数字が、あとで大きな手がかりになります。

よくあるズレ方を、幅で見分ける

「ズレている」と言われたときに最初にやることは、犯人探しではなく採寸です。何時間、あるいは何日ズレているのか。ここで場所がほぼ決まります。

ちょうど9時間ズレている → タイムゾーン設定の食い違い

もっとも多い型です。どこかの層が UTC のまま動いています。

分かりやすいのは、深夜0時から朝9時までに起きたデータだけがおかしくなる現象です。日本の8月22日 2時は、UTCでは8月21日の17時。日付が変わってしまうので、「深夜に登録した分だけ、前日の集計に入る」という形で表に出ます。

日中に登録した分は日付が変わらないため、昼間にテストしても再現しません。これが「テストでは出ないのに本番で出る」の正体であることが、かなりあります。

ちょうど1日ズレている → 日付の「境界」の扱い

時刻ではなく日付だけがズレている場合は、集計の範囲の切り方を疑います。

月末・月初だけおかしい → 月の加算・減算

「1か月前」「翌月」を計算しているところは、月末が絡むと素直に動きません。

たとえば1月31日に1か月足すと、実装によっては「2月31日」がいったん作られ、それが補正されて3月2日や3月3日になります(2月が何日あるかで変わります)。月次のバッチが月末に走る現場では、年に数回だけ結果がおかしくなるという形で現れます。再現性が低いので、原因不明のまま放置されがちなところです。

半端な時間ズレる・特定の時期だけ → 海外のサマータイム

外部サービスや海外拠点のデータが絡む場合は、サマータイム(DST)を疑います。米国や欧州には、年に2回、時計が1時間動く日があります。

その日には、存在しない時刻(1時間飛ぶ)と、2回ある時刻(同じ時刻が繰り返される)が生まれます。「年に2日だけ、集計が二重になる」「その日のデータだけ登録できない」は、この形です。日本の現場でも、海外SaaSのAPIから受け取る日時では十分起こります。

バラバラにズレる → タイムゾーンではないかもしれない

幅が一定でないなら、いったんタイムゾーンから離れます。バッチの実行時刻とデータの投入時刻が競合している、キャッシュが古い日付を返している、といった別の原因のほうが近いことがあります。この場合はキャッシュが原因の「直したのに直らない」の切り分けのほうが近道です。

調査の手順

① ズレの幅を測る

まず、確実に事実だと言えるものを1件つかまえます。

地味ですが、ここを飛ばして設定を眺めはじめると、時間が溶けます。差の数字が出るまでは、まだ調査が始まっていないくらいに思っておくとちょうどいいです。

② 生のデータを見る

次に、画面に出ている値ではなく、DBに入っている値そのものを見ます。画面の表示は加工されたあとなので、どこでズレたのか分かりません。

-- MySQL:接続のタイムゾーンと、いまの時刻の見え方を確認する
SELECT @@global.time_zone, @@session.time_zone, NOW(), UTC_TIMESTAMP();

-- 実際のレコードを1件見る(カラム名・テーブル名は自分の環境に置き換える)
SELECT id, created_at FROM orders WHERE id = 12345;
-- PostgreSQL:同じことを確認する
SHOW timezone;
SELECT now(), now() AT TIME ZONE 'UTC';

ここで見たいのは、入っている値が日本時間なのか、UTCなのかです。深夜1時に登録されたはずのデータが 16:00 になっていれば、UTCで保存されています。

もうひとつ、カラムの型も確認しておきます。型によってDBの振る舞いが変わります。

「保存はUTCで、表示だけ日本時間に変換する」は、正しい設計のひとつです。UTCで入っていること自体はバグではありません。 問題になるのは、変換するはずの層が変換していない、あるいは二重に変換しているときです。

③ 4つの層の設定を突き合わせる

幅が9時間なら、ここで犯人が見つかります。上から順に確認します。

# サーバー(OS)のタイムゾーン
timedatectl        # systemd 環境
date +"%Z %z"      # どの環境でも使える簡易確認

# cron の実行時刻がどのタイムゾーンで動くか(基本はサーバーのタイムゾーン)
sudo crontab -l
# PHP の既定タイムゾーン
php -i | grep -i "date.timezone"
php -r 'echo date_default_timezone_get(), PHP_EOL;'

# Node.js
node -e 'console.log(process.env.TZ, Intl.DateTimeFormat().resolvedOptions().timeZone)'

# Python
python3 -c "import datetime; print(datetime.datetime.now().astimezone().tzinfo)"

アプリの設定は、フレームワークの設定ファイルが優先されることが多い点に注意してください。php.iniAsia/Tokyo でも、フレームワーク側の設定で UTC に上書きされていれば、動くのは UTC です。「php.iniを見たから大丈夫」で終わらせず、実際に走っているプロセスから値を出すのが確実です。

DBについては、②で確認した @@session.time_zoneSYSTEM になっていることがあります。これは「サーバーのOSに合わせる」という意味なので、DBサーバーのOS設定を見に行く必要があります。また、Asia/Tokyo のような名前でタイムゾーンを指定したいときは、MySQLではタイムゾーンテーブルの読み込みが必要な場合があります。名前で指定してエラーになるときは、+09:00 のようなオフセット指定で確認すると切り分けが進みます。

コンテナで動かしている場合は、ホストのタイムゾーンがコンテナに引き継がれないのが既定です。イメージの中は UTC のまま、というのはよくある構成です。ここは環境差で事故らない設定管理の話とつながります。

④ ズレる時間帯で再現する

原因の見当がついたら、再現させます。再現しないまま直すと、直ったかどうか確かめられません。

検証環境が本番と同じタイムゾーン設定になっているかは、先に確認してください。ここが違うと、再現しなかった理由が分からなくなります。

⑤ 直す場所をひとつに決める

原因が分かったあと、直せる場所は複数あります。DBの設定を変える、アプリの設定を変える、SQLを直す、表示側で変換する。どれでも症状は消えます。だからこそ、どこで直すかを先に決めて、1か所だけ触るのが安全です。

判断の目安はこうです。

いちばんやってはいけないのは、表示側で「9時間足す」を書いて辻褄を合わせることです。その場は直りますが、根っこが直った日に、今度は9時間ズレて戻ります。数年後の担当者(自分かもしれません)が、なぜ9時間足しているのか分からずに悩みます。応急処置としてやむを得ないときは、コメントに理由と、いつまでの暫定かを書き残しておくと、まだ救いがあります。暫定対応と恒久対応を分けて管理するのやり方が、そのまま使えます。

影響:見つけにくいわりに、じわじわ効く

日付のズレは、システムが止まるタイプの障害ではありません。だからこそ、気づかれないまま長く続くことがあります。

逆に言えば、一度きちんと突き止めておくと、同じ形のバグは以後まとめて片づきます。4つの層の設定を書き出しておけば、次に「1日ズレる」が来たとき、幅を測って設定表を見るだけで終わります。今回の調査は、そこそこ割に合う仕事です。

明日やること

障害が起きていなくても、明日30分で確かめておけることがあります。

全部やらなくて大丈夫です。1つ目の「並べる」だけでも、現場の見通しはかなり変わります。

今日のチェックリスト

上から優先順位で並べてあります。まずは最低ラインだけで十分です。

まずここだけ(これだけで効きます)

次に確認したい

余力があれば

最低ラインの3つが済んでいれば、もう原因は半分絞れています。残りは、余裕のある日に少しずつで大丈夫です。

日付のズレの原因を突き止め、集計の数字が合ったことを確認して穏やかな表情を見せる一人運用の保守担当者

日付のバグを追いかけているとき、いちばんしんどいのは「自分の書いたコードが読めていないのでは」と思えてくる時間だと思います。でも、この種のバグは読解力の問題ではありません。目に見えないところで、時計が4つ動いているだけです。見えないものは、読んでも見えなくて当たり前です。

幅を測って、生の値を見て、設定を並べる。この3つを順にやれば、たいていのズレは正体を現します。今日その1件を突き止められたなら、同じ形で困っている未来の何件かも、まとめて片づいています。全部を一度にきれいにしなくて大丈夫です。まずは、4つの時計を並べて見るところから。

よければ、こちらも

保守運用の実務ヒントを、メールでお届けしています。よかったら受け取ってください。