毎年1月1日に私のAPIが壊れた話

私のAPIは、1月1日のUTC 00:00ちょうどに壊れました。

ユーザーの深夜に壊れたのではありません。UTCの深夜に壊れたのです。東京のユーザーは、現地時間の午前9時からデータが壊れていることに気づきました。

テストはパスしました。ステージング環境でも問題ありませんでした。エラーが本番環境にのみ存在したのは、本番サーバーがステージングとは異なるタイムゾーンを使用していたからです。

問題はこのロジックにありました:

function getDailyReport(date) {
  const start = new Date(date).toISOString().split('T')[0];
  const end = new Date(start + 'T23:59:59Z');

  return db.reports.findMany({
    where: {
      createdAt: { gte: new Date(start), lt: end }
    }
  });
}

時刻を指定せずに "2026-01-01" のような日付を渡すと、システムはローカルのタイムゾーンを使用します。

私たちのステージングサーバーはUTCを使用していました。一方、本番サーバーはUS-Eastを使用していました。これにより、すべての日のクエリにおいて5時間のオフセットが発生していました。

すべてのクエリで5時間分のデータを失っていました。数値の差がわずかだったため、丸一年間誰も気づきませんでした。

修正は簡単でした。文字列に時刻を付加して、UTCを強制することです:

function getDailyReport(date: string) {
  const start = new Date(`${date}T00:00:00Z`);
  const end = new Date(`${date}T23:59:59.999Z`);

  return db.reports.findMany({
    where: {
      createdAt: { gte: start, lt: end }
    }
  });
}

コードの変更は数秒で済みましたが、システムの修正には1週間かかりました。私たちは以下の4つの対策を行いました:

• CIにタイムゾーンのアサーションを追加し、環境が常にUTCであることを保証するようにした。 • すべてのコンテナを同一に保つため、すべてのDockerfileで TZ=UTC を設定した。 • デプロイスクリプトにタイムゾーンのチェックを追加した。 • タイムゾーン情報が欠落している Date コンストラクタを検知するリンタールールを作成した。

タイムゾーンのバグは危険です。システムをクラッシュさせることはありません。正しく見える「間違ったデータ」を生成するからです。ユーザーはエラーページを見ることはありません。誤った数値を目にし、それを信じてしまうのです。

次の3つのルールに従ってください:

  • システムのタイムゾーンを決して信用しないこと。常に TZ=UTC を設定すること。
  • タイムゾーンなしで日付をパースしないこと。
  • CIのタイムゾーンが本番環境と一致していると思い込まないこと。

出典: https://dev.to/kollittle/my-api-broke-every-january-1st-the-timezone-bug-i-should-have-caught-in-code-review-51hb