title: "UTC日付生成で深夜アクセスが前日に吸われる現象の直し方"
emoji: "🕒"
type: "tech"
topics: ["Cloudflare", "TypeScript", "Dayjs", "個人開発"]
published: false
個人開発で公開しているWebツールの利用動向を追うため、閲覧数や特定ボタンのクリックイベントを日次で記録する計測基盤を本番へ反映した。
深夜の作業を終えて2026年9月13日の午前0時52分に届いた記録を本番環境で確かめたところ、保存された日付カラムに「2026-09-12」と書き込まれていた。
昨日付に吸い込まれている。
このままでは翌朝に宣伝を流した際、午前0時から9時までの閲覧や操作ログがすべて前日側の集計行に合算され、同じ日に起きたはずの数字が2行に割れて読めなくなる。
0時台のログが前日に記録される原因

日本時間の午前0時から9時まではUTCでは前日扱いとなり日付が1日ずれる
原因は日付文字列の組み立て方にあった。内部処理で日次の記録キーを生成する際、タイムゾーンの考慮を省いてUTC基準の日付を取得していた。
日本標準時(JST)は協定世界時(UTC)より9時間進んでいる。日本時間の9月13日午前0時52分は、UTCでは9月12日午後3時52分だ。
午前9時を迎えるまでの全イベントが、UTC基準では前日の日付として判定されてしまう。
利用しているエッジ環境の挙動もこのズレを後押ししていた。Cloudflare Workersの本番環境ではDate APIが常にUTCを返す仕様になっており、実行環境側で日本時間をよしなに解釈してくれる余地はない。さらに日付ライブラリの処理でも、Cloudflare Workers環境におけるDay.jsのタイムゾーン制御のように明示的なプラグイン有効化や時差計算を挟まないとUTCへ吸い寄せられる。ローカル検証ではマシンの環境変数に引っ張られて素通りしていた処理が、本番へ出した瞬間に露出した。
日付の扱いを甘く見ていた。
書き込み修正だけで終わらない罠

書き込みと読み取りの双方が別々にUTCを使っており片方だけの修正では割れたままになる
日付の組み立て箇所だけを日本時間基準に変えれば済むと考え、修正に着手した。だがログの集計パイプラインを追っていくと、もう1つの落とし穴が見つかった。
保存されたログを抽出して集計テーブルへ流し込む読み取りスクリプトの側でも、日付の絞り込み条件に素のUTCを使っていた。
片方だけ直しても数字は割れたままだ。
書き込み側だけをJSTに直して「2026-09-13」と刻印しても、読み取り側のスクリプトがUTC基準で日付境界を切っていれば、集計の取りこぼしが発生する。逆に読み取り側だけを直せば、書き込まれた前日付のレコードが永遠に抽出範囲から漏れる。
書き込みと読み取りの2箇所が別々にUTCを抱え込んでいたため、両方の時間を同時に揃えなければ1日の集計行は綺麗に1本化されない。
共通関数による時間軸の統一

共通関数で生成を1箇所に集約し読み取り側にも+9時間を足して境界を一致させた
日次の境界計算を散らばらせないよう、日付操作モジュールに businessDay という共通関数を新設した。
export function businessDay(date: Date = new Date()): string {
const jst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
return jst.toISOString().slice(0, 10);
}
閲覧数を記録するテーブルとイベント追跡用のテーブルの双方で、この共通関数を経由して日付キーを確定させる形へ変更した。あわせて集計読み取りスクリプトの日付絞り込みクエリにも +9 hours の時間加算を明示的に足し、書き込みと読み取りの境界線を完全に一致させた。
型検査とリントを通過させ、新規の境界値テスト3件を加えた計167件のテストを実行して全てパスした。
本番ビルドの成功後、午前0時台にイベントを発火させて再検証したところ、9月13日の行に正しいカウントが1行で積み上がる状態へ直った。
夜中に叩いた動作確認ログを、昨日の自分へプレゼントし続ける事態だけは避けられた。
