目次
カプログという、デートのルートや訪問場所をまとめて投稿・共有するWebサービスを運営している。2026年5月11日の深夜から12日の未明にかけて、そのカプログで本番障害を起こした。
きっかけは、投稿の保存ボタンだった。押しても保存されない。ページを見るぶんには何も起きていないように見えるのに、保存だけが失敗する。そして気づいたときには、保存とは関係ないはずのTOPページまで、開かずにずっと読み込み続ける状態になっていた。
原因として一番怪しいものは特定できた。ただし最後まで「最有力仮説」のままで、確定はしていない。この記事は、犯人を突き止めた話ではなく、犯人を確定できないまま、その事故が起きた経路ごと設計を捨てた話だ。
日本語入りURLを本番に出した夜
当時のカプログの投稿URLは /posts/{UUID} だった。機械的なIDの羅列で、URLを見ても何の投稿かわからない。そこから、日本語を含むslugに短い公開IDをつなげた /posts/{日本語slug}-{public_id} という形式への移行を進めていた。
移行は段階を踏んでいて、この夜に出したのは、新形式のURLを生成しつつ、既存のUUID形式のURLも引き続き受け付ける「両受け」の変更だった。commitの記録は5月12日の0時29分。深夜に本番へ出している。
URLの形式が増えるだけで、データ構造は変わらない変更だった。実際、新しい日本語入りURLでの投稿詳細の表示は、問題なく通っていた。
「見る」は生きて、「保存」だけが死んだ
異変は表示ではなく、書き込みで起きた。
新しいURLで開いた投稿は、詳細ページも編集画面も正常に表示される。確認したGETは通る。ところが編集画面で保存ボタンを押すと、その保存だけが失敗する。同じ画面で、読む操作と書く操作の生死が分かれた。
Cloudflare側のログには exceededCpu が多発していた。canceled も出ていて、wallTimeは最大39秒。アプリ側のエラーは TypeError: Cannot read properties of undefined (reading 'apply') という、一見URLとは何の関係もなさそうな顔をしたものだった。
そして巻き添えが始まる。TOPの / や /posts/* へのRSCリクエストが道連れになり、/terms?_rsc や /privacy?_rsc といった、利用規約やプライバシーポリシーのような静かなページのリクエストまで失敗の記録に並んだ。体感としては「caplog.jpが開かず、ずっとロードし続ける」。保存ボタンひとつの障害が、サイト全体の障害になっていた。
Workerを一つ前の安定版に戻すと、サイトは正常に戻った。そして戻した環境では、旧UUID形式のURLでの保存は普通に成功した。同じ保存処理で、UUIDのURLなら通り、日本語入りのURLなら死ぬ。ここで、障害が新URL経路に固有のものだと切り分けられた。
原因は最後まで「最有力仮説」のまま
ログと挙動から組み立てた推定は、こうだ。
保存ボタンを押すと、Next.jsのServer Actionが動く。そのPOSTは、いま開いている日本語を含むURLに対して行われる。この経路でだけ、Action IDから実行する関数を引き当てる処理が失敗して、関数が undefined になる。undefinedに対して .apply を呼ぶから、あの TypeError が出る。そしてこの失敗はリトライとRSCのfallbackのループになり、CPU時間を食い潰して、他のリクエストまで巻き添えにする——。
この仮説なら、GETは通るのに保存のPOSTだけが失敗したことも、exceededCpu も、静かなページの巻き添えも、一つの流れとして説明できる。
ただ、これは説明がつくというだけで、証明ではない。Next.js 16とOpenNextとCloudflare Workersの組み合わせのどの層に原因があるのか、切り分けの再現実験はしていない。だからどのソフトウェアのバグだとも書けないし、書かない。最小構成で再現させて原因の層を特定する道もあったはずだが、僕はそれをやらなかった。直すべきはそこではない、と判断したからだ。
直すのをやめた
止血はWorkerのrollbackで済んでいた。コードをrevertする必要はなくて、実際、この変更で書いたURL生成のhelperは後の作り直しでそのまま流用している。壊れたのはコードの個々の部品ではなく、「日本語を含むURLにServer ActionがPOSTする」という経路そのものだった。
ここで進路が二つあった。日本語URLを安全に通す方法を探し続けるか、日本語URLをやめるか。
前者は、原因が仮説のままである以上、修理できたかどうかの判定すら難しい。エンコードの工夫で表示が通っても、また「見るのは通るが保存だけ死ぬ」が起きない保証がない。少なくとも今回は、表示が正常なことだけでは見抜けなかった。
だから後者にした。とはいえ、代わりのURLをどうするかはすぐには決まらなかった。日本語slugを英語に置き換える案、slugをAIに生成させる案、ユーザーに入力してもらう案、カテゴリやタグやエリアで階層を作る案、いっそ公開IDだけにする案。いくつも並べて、しばらく行ったり来たりした末に、既存のデータから機械的かつ決定的に生成できる形へ寄せていった。
落ち着いた先が、食べログ型のASCII階層URL /posts/{都道府県}/{市区町村}/{公開ID} だ。たとえば東京・新宿の投稿なら /posts/tokyo/shinjuku/{public_id} になる。都道府県は47件の固定テーブルで引く。市区町村はreverse geocodingの英語表記をベースに、揺れる箇所だけ手元の補正表で上書きする。表記はヘボン式で、長音は省略して tokyo osaka kyoto。どの要素も投稿が既に持っている位置情報から決定的に生成され、AIもユーザーの手も挟まない。そして一度発行したURLは変えない。
日本語URLの「読める良さ」は失った。でも tokyo/shinjuku なら読めるし、揺れないし、何よりServer ActionのPOST先にASCIIしか現れない。
10段階に割って作り直す
一度本番を壊した直後だったから、作り直しは細かく割った。日付が変わった後の同じ5月12日に最初の段階が始まって、全体は10段階になった。
DBに都道府県と市区町村の列をNULL許容で足すだけの段階。壊れた新URL経路をrouteから削除するだけの段階。投稿作成時に地域を生成する段階。既存投稿のbackfill。NOT NULLとCHECK制約。新しい階層routeの追加。内部リンク13ファイルの切り替え。canonicalとsitemap。旧UUID URLから新URLへの308リダイレクト。各段階にdeployと疎通確認を挟んで、5月14日に最後まで通った。
正直に書くと、この作り直しの途中でもう一度本番障害を起こしている。新しい階層routeを足した段階で、Next.jsのdynamic segmentの命名衝突を踏んだ。ただ今回はCloudflareのログでエラー8件を検知して、手動でrollbackした。慎重になったから事故がゼロになった、という話ではない。段階を割っていたから、切り戻す対象をその段階に限定できた、という話だ。
もう一つ正直に書くと、このとき既存投稿は10件だった。10件のbackfillのために専用スクリプトを書き、適用後に残NULLゼロと形式不正ゼロを確認するSQLを流した。規模だけ見れば手作業で済む数だ。それでもこの手順にしたのは、直前に本番を壊したばかりだったからだ。
一番変わったのはdeployのやり方だった
コードやURLより、長く残った変化は運用のほうにある。
この障害の後の復旧では、まずProduction Deployに自分で禁止フラグを立てた。安全な段階のdeployが通ったことを確認して第一段階を解除し、さらに様子を見て全面解除する二段構えにした。
障害を起こしたWorkerのversionは、「rollback禁止」として台帳に載せた。障害対応で慌ててrollback先を選ぶとき、履歴一覧には障害版も普通に並んでいる。深夜の自分がそれを掴まないように、選んではいけないversionを名指しで書き残しておく。この台帳は、作り直し途中の二度目の障害で2件になった。
deploy後の確認にも、Cloudflareのログにあの障害のエラー群——exceededCpu や例の TypeError——が出ていないかを見るチェックが加わった。この頃から、カプログのdeployは「出して、開いて、大丈夫そう」から、決まった疎通項目とログ確認を通す形に変わっていった。
カプログの開発はAIに書かせている部分が大きく、コードができる速度は上がり続けている。だからこそ、本番に出る直前の一点だけは、意図的に遅くする方向に倒した。
URLに日本語がない理由
GETが200を返すことは、その経路が安全だという意味ではなかった。表示では問題が出ていなかった経路の、書き込み側だけが死んでいて、しかも死に方がCPU食い潰しだったせいで、無関係なページまで巻き込まれた。
そして、原因を確定できなくても、そこで調査を終える理由はあった。仮説止まりの原因を追い詰めて日本語URLを安全にする競争を続けるより、その経路がなくても成立する設計に作り直すほうが、確実に安全だった。
この記事は、日本語slugを安全に通す方法の話ではない。それを通さなくても読めるURLは作れると分かった話であり、カプログの投稿URLに今も日本語が一文字もない理由の話だ。