切れたSSEをすぐ止めた

公開: 2026-09-05
SSE運用Next.js障害対応

SSEの接続が切れると、ついタイムアウトを長くしたくなる。 でも今回の問題は、待っても成功しない要求を「一時的な切断」に見せていたことだった。

/api/mcp のGETがSSEを開いた直後に閉じ、クライアントが2秒ごとに接続を作り直していた。 接続自体は作れるので、監視上は一見すると動いているように見える。実際には、成功しない経路が実行時間だけを使い続けていた。

GETでSSEを開く直後に接続が切れる405で即時に拒否2秒ごとの再接続を繰り返す再試行の合図を出さない続けられない経路は、接続後に切るより、接続前に明示して終える。
問題は切断そのものではなく、成功しない要求に再試行を促していたことだった。

接続できたことを成功にしない

SSEでは、ソケットが開いたことと、要求を処理し続けられることが別だ。 今回は未対応のGETがストリームを開いてから閉じていたため、クライアントは一時的な障害だと解釈した。

先に切り分けたのは3点だけだった。

  1. 接続を閉じたのはクライアントか、サーバか
  2. 再試行の間隔は何秒か
  3. そのGETを継続処理できる仕様が本当にあるか

この順に見ると、「SSEが不安定」という大きすぎる説明を、未対応の要求を受け付けていたという小さな原因に戻せる。

続けられない要求は405で終える

対応は、GETをSSEとして受け入れず405で返すことにした。 速く失敗させるのは冷たい挙動ではない。成功できない要求に対して、再接続すれば回復するという誤った合図を送らないための応答だ。

修正後は、拒否される経路の応答時間が短くなり、接続を作って閉じるループが止まった。正しい接続経路は別に残し、未対応の要求だけを狭く拒否した。

検証は3つに分ける

「405が返った」だけでは足りない。次の3つを分けて確認した。

この分け方は、RLSやデプロイでも同じだ。1本の成功ログを完了証拠にせず、拒否すべき側と続ける側を並べる。RLSとアプリ側の絞り込みでも、見える側と見えてはいけない側を別々に確かめている。

公開された状態まで確認する手順は、公開されたか確かめるに残した。作ったものの一覧はプロダクトから見られる。

次に同じ負荷を作らないために

ストリームを実装するときは、開始できるかだけでなく、続けられない要求をどう終えるかも仕様に入れる。 接続後に黙って切るより、接続前に理由を返す。その1行の境界が、再試行の積み上がりを防ぐ。