毎年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のタイムゾーンが本番環境と一致していると思い込まないこと。