WebSockets と SSE と長時間ポーリング — チャットアプリが溶ける前に正しいリアルタイムパターンを選んでください
多くの開発者は、WebSocket をデフォルトで選択しています。WebSocket、サーバー送信イベント、長時間ポーリングの違いについての解説は、遅延、再接続行動、インフラの複雑さ、双方向性といった実際のトレードオフをカバーしており、適切なパターンを選択し、データフィードの過剰設計を避けられます。
あなたはダッシュボードを作成しました。ユーザーはリアルタイムのデータ更新が必要です。あなたはWebSocketsに手を伸ばしました——なぜならそれが誰もがやっているからです。しかし今、2時半にバグを調査しています。なぜならロードバランサがクライアントを間違ったポッドに送っているからです。あなたが送信しているデータは、10秒ごとに1つのJSON数値です。WebSocketsは過剰でした。
これは常に起こっています。開発者はWebSocketsをリアルタイム解決策のデフォルトとして扱いますが、SSEや長時間ポーリングの方が早く導入でき、スケーラビリティが高く、運用コストも低かったのです。ここに実際に選択する方法を示します。
決定表
深掘りに入る前に、完全な比較をここに示します。この情報をもとに判断を下し、必要であれば以下のセクションを読みましょう。
| 特徴 | 長距離ポーリング | サーバー送信イベント(Server-Sent Events) | WebSockets |
|---|---|---|---|
| プロトコル | HTTP/1.1 | HTTP/1.1+(チャンク送信) | WS(HTTPアップグレード) |
| 方向 | クライアントが引き取るだけ | サーバーからクライアントへのみ | 完全な両方向通信 |
| ブラウザサポート | すべてのブラウザ | すべての現代ブラウザ(IE11を除く) | すべての現代ブラウザ |
| 自動再接続 | 手動——ループを自分で書く | EventSourceを用いて組み込み | 手動またはライブラリを用いる |
| CDN/プロキシに優しい | はい | ほとんど(タイムアウトの監視) | いいえ |
| 水平スケーリング | ステートレス、非常に簡単 | ステートレス、非常に簡単 | ステイキーリンクやpub/subブローカーが必要 |
| インフラの複雑さ | 低——シンプルなHTTP | 低——シンプルなHTTP | 高——ステートフル接続、LB設定、ブローカー |
| 遅延 | ポーリング間隔+リターンループ | リアルタイムに近い | リアルタイムに近い |
| 最適な用途 | 低頻度更新、古くからのインフラ | ダッシュボード、フィード、通知 | チャット、共同編集、ゲーム |
長時間ポーリング:見栄えのないワークホース
長時間ポーリングは「古い」または「間違っている」とは言えません——それは単にHTTPです。クライアントがリクエストを送り、サーバーが何かを言うまでまたはタイムアウトが発生するまでそのリクエストを保持し続けます。クライアントが応答を受け取り、すぐに次のリクエストを送ります。繰り返します。
これはWebSocketsが存在する前から存在しており、今もまだ数百万の生産アプリで稼働しています。理由は、それは単なるHTTPだからです。あなたの既存のリバースプロキシ、CDN、ロードバランサ、監視ツールすべてが設定なしで理解できるからです。ステイキーリンクやプロトコルアップグレード、WSを理解するインフラは不要です。
しかし、デメリットは現実です。すべての「更新」はフルHTTPリクエストサイクルになります——ヘッダー、TCPのオーバーヘッド(HTTP/2では除く)、サーバー側のリクエストキュー。高並列性の下ではサーバー側で高コストになります。また、遅延の下限は各ポーリングのリターンタイムであり、イベント自体の遅延ではありません。5秒ごとにポーリングしている場合、イベントが1秒目に発生したとしても、ユーザーはさらに4秒待たなければなりません。
使用する場合: 古くからのシステムで、永続接続層を追加するコストがインフラ変更を上回る場合。低頻度更新(1分未満)。SSEが過剰なプロキシによって殺される環境。クライアントが明示的に制御するポーリング——ジョブステータスチェック、非同期タスク結果。
サーバー送信イベント:誰も忘れてしまうもの
SSEは標準(HTML Living Standard)であり、サーバーが単一の長期間のHTTP接続を通じてテキストイベントのストリームを送信できるようにします。ブラウザは自動的に再接続を処理します。あなたは再接続ループを書く必要はありません—— EventSource APIは、最後に見つけたイベントIDを使って自動的に再接続し、ネットワークのブリクに続き、ストリームを再開できます。
通信形式は驚くほどシンプルです:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
id: 1
event: price-update
data: {"symbol":"BTC","price":67420}
id: 2
event: price-update
data: {"symbol":"ETH","price":3521}
各イベントは、オプションの id, event, dataと、 retry フィールドを含むシンプルなテキストで、空白行で区切られています。SSEエンドポイントはcURLで直接テストできます——適切なAcceptヘッダー( cURLコマンドビルダー )を用いてリクエストを作成し、レスポンスをストリーミングします。text/event-streamクライアント側:
SSEには一つの本質的な制限があります:それはサーバーからクライアントへのみです。クライアントは同じ接続を介してデータを送信できません——クライアントが発動するすべてのアクションは通常のHTTPリクエストを通じて行われます。ほとんどのダッシュボード、通知、フィード用途では、これは問題ありません。あなたはそもそもクライアントからサーバーへ送信する方向を使いませんでした。
const es = new EventSource('/api/events');
es.addEventListener('price-update', (e) => {
const payload = JSON.parse(e.data);
updateDashboard(payload);
});
es.onerror = () => {
// EventSource will auto-reconnect. You don't need to do anything here
// unless you want to log or update UI state.
};
もう一つの注意点:一部のプロキシはレスポンスボディをバッファリングし、接続が閉じられた後にのみそれを転送するため、ストリーミングが完全に破綻します。NGINXには
が必要です。Cloudflareの無料プランはSSEをバッファリングします。もし、インフラにバッファリングプロキシがあり、それを設定できない場合、SSEは機能せず、WebSocketsまたは長時間ポーリングに切り替える必要があります。 proxy_buffering offリアルタイムダッシュボード、通知フィード、アクティビティログ、AI応答ストリーミング(これはChatGPTのストリーミングAPIが実際に使用しているもの)、株価タイマー、デプロイ/ビルドログのテール。サーバーがデータを送信し、クライアントがデータを送信する必要がない場面すべてに適用されます。
使用する場合: WebSockets:実際に価値がある場合
WebSocketsは、単一のTCPソケット上でフルダブル方向の持続的な接続を提供します。HTTPアップグレードハンドシェイクの後、双方がフレームメッセージをいつでも送信できます。遅延はネットワーク上で最も低いレベルに抑えられます——各メッセージに新しいリクエストやヘッダーが不要です。
代償として、それらはステートフルです。サーバーはすべてのオープン接続を追跡する必要があります。これはスケールの問題を即座に引き起こします:ロードバランサは接続(リクエストではなく)に基づいてルーティングするため、ステイキーリンクまたはpub/subブローカー(Redisなど)が必要になります。あなたのCDNはWebSocketsトラフィックをキャッシュできません。リバースプロキシはアップグレードに明示的な設定が必要です。監視ツールがWSフレームを理解していないと、パラメータレベルの可視性を得られません。
これらすべては克服可能ですが、現実の問題です。プロジェクトを開始し、選択肢を評価する際、「それは単なるHTTP」vs「ステートフルWS接続とRedis pub/subレイヤー」は複雑性の重要な違いです。
WebSocketsのパレットは通常JSONです。もしWebSockets統合をデバッグし、メッセージを確認またはフォーマットする必要がある場合、
は開発ツールのネットワークタブで最小化されたパレットを読みやすくします。 JSONフォーマッタ チャットアプリケーション(すべてのメッセージがクライアントが発動するもの)。共同編集(Figma、Google Docs — 複数のクライアントが共有状態を書き換える)。マルチプレイヤーゲーム(高頻度、低遅延の状態同期)。金融取引端末(オーダーボードの100ms未満の遅延)。基本的に、クライアントが頻繁にデータを送信する必要がある場面すべて。
使用する場合: スケーリングの問題:誰も言及しないが、遅れてから気づくもの
長時間ポーリングとSSEはHTTP層でステートレスです。各リクエストは任意のサーバーインスタンスに到達できます。あなたの水平スケーリングストーリーは単純で、それは特徴です:インスタンスを追加すれば完了です。接続ステートはオープンHTTP接続の期間だけ存在し、ロードバランサは自由にルーティングできます。
WebSocketsはステートフルです。クライアントAの接続はサーバー1にあります。あなたがサーバー2からクライアントAにメッセージを送りたい場合(イベントがそのサーバーで発生した場合)、サーバー2はその接続について知らなければなりません。標準的な解決策はpub/subブローカー——Redis pub/sub、Kafka、またはPusherやAblyなどの管理サービスです。あなたはそれらのレイヤーを自作するか、それを使う必要があります。
Pusherは500コンカレント接続に対して$49/月を請求しています。Ablyも同様の価格です。もし「リアルタイム機能」が誰かが投稿にいいねしたときに通知バッジが更新されるようなものであれば、SSEを無料でホストできるAPIと併用するだけで、それほど多くの費用はかかりません。
HTTP/2が長時間ポーリングの計算を変える
クラシックHTTP/1.1の長時間ポーリングは、1つの接続あたり1つのリクエストを保持します。HTTP/2では、1つのTCP接続上で複数のストリームが存在します。オーバーヘッドは低くなります。これは長時間ポーリングをSSEよりも選ぶ理由ではありませんが、2024年において「HTTPのオーバーヘッドが高価」という批判は2012年に比べて適用が小さくなっています。
あなたがHTTP/2で端末からサーバーまで(中間プロキシがダウングレードしない)の場合、長時間ポーリングは多くの古いベンチマークが示すよりもスケーラブルです。
迅速な決定ガイド
あなたの用途が以下に合致する場合、ここを読み終えます:
更新頻度 < 1分、古くからのインフラ、シンプルなポーリングセマンティクス:
- 長時間ポーリング。複雑にしないでください。 サーバーがデータを送信し、クライアントが主に読み取りを行う、標準的なHTTPインフラ:
- SSE。これは80%の「リアルタイム」機能をカバーしています。 クライアントが頻繁にデータを送信し、100ms未満の遅延が重要で、真正な両方向通信が必要:
- WebSockets。今や複雑性が価値になります。 AIストリーミング(OpenAI、Anthropicなど)を使用している場合:
- SSE——それは実際にそのストリーミングAPIが使用しているものです。 マルチプレイヤーゲームまたは共同編集者:
- WebSockets、または特別なフレームワーク(Liveblocks、PartyKitなど)。 デフォルトの答えはWebSocketsではありません。それはSSEです——あなたがその用途に十分でない理由がない限り。実際にフル両方向リアルタイムを必要とする生産アプリの数は、WebSocketsを必要とするアプリの数よりもはるかに少ないです。そのギャップは、2時半にロードバランサの設定の前で蓄積されたエンジニアリングの負債です。
デフォルトの答えはWebSocketsではありません。SSEです。特定の理由がない限り、それが十分です。実際に完全な両方向リアルタイムが必要な生産環境アプリケーションの数は、それらの数に比べてはるかに少ないです。 をリポジトリにラインエンドポリシーとして設定してください。そして WebSockets。そのギャップは、ロードバランサの設定の前に午前2時を過ぎたときに蓄積されたエンジニアリングの負債です。
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
