
WebSocket・SSE・ポーリングの使い分け - リアルタイム通信の選び方
チャット、通知、株価、進捗バー——「サーバーの変化をすぐ画面に反映したい」とき、選択肢は大きく ポーリング・SSE・WebSocket の3つです。なんとなく WebSocket を選びがちですが、用途によっては SSE のほうが圧倒的に楽なこともあります。この記事では、MDN・RFC 6455 / 6202 を一次ソースに、3方式の違いと選び方を整理します。
3つの方式
ポーリング(short / long polling)
- Short polling: クライアントが一定間隔でHTTPリクエストを繰り返す。データが無くてもサーバーは即レスポンスを返す。最も単純だが、更新が無いときも通信が走る
- Long polling(RFC 6202): サーバーがイベント発生かタイムアウトまでレスポンスを保留する。クライアントは受信したらすぐ次のリクエストを送る。安全なタイムアウト値の目安は30秒前後
SSE(Server-Sent Events / EventSource)
- サーバー → クライアントの単方向ストリーム
- HTTP の上で動き、
Content-Type: text/event-streamを返すだけ - 自動再接続が標準装備。
Last-Event-IDで中断点から再送できる - テキスト(UTF-8)のみ。バイナリは Base64 等にする必要がある
WebSocket
- 全二重(双方向同時)。
ws:///wss:// - HTTP からの Upgrade ハンドシェイク(
101 Switching Protocols)で専用プロトコルに切り替える - バイナリフレーム対応。チャット・ゲーム・協調編集などに向く
比較表
| 項目 | Short Polling | Long Polling | SSE | WebSocket |
|---|---|---|---|---|
| 通信方向 | 単方向 | 単方向 | 単方向(サーバー→) | 双方向 |
| プロトコル | HTTP | HTTP | HTTP(text/event-stream) | ws / wss |
| 自動再接続 | 自作 | 自作 | 標準装備 | 自作 |
| バイナリ | 可 | 可 | 不可(テキストのみ) | 可 |
| オーバーヘッド | 毎回大 | 大(頻度は低) | 接続時のみ | 接続時のみ |
| 実装の手軽さ | 最も簡単 | やや複雑 | シンプル | サーバー側がやや複雑 |
| HTTP/2の恩恵 | 多重化 | 多重化 | 接続数上限が解消 | 無関係(独立プロトコル) |
コード例
Long polling(クライアント)
async function longPoll() {
try {
const res = await fetch("/events");
handleData(await res.json());
} catch {
await new Promise((r) => setTimeout(r, 1000)); // 失敗時は少し待つ
}
longPoll(); // 常に次を投げる
}
longPoll();サーバーはデータが来るまでレスポンスを保留し、タイムアウト(例: 30秒)で空を返します。
SSE(クライアント)
const es = new EventSource("/events");
es.onmessage = (e) => console.log(e.data);
// 名前付きイベント
es.addEventListener("price", (e) => console.log(JSON.parse(e.data)));
es.onerror = (err) => console.error("SSE error", err);
// 不要になったら es.close();SSE(サーバーの応答形式)
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
: コメント(ハートビートにも使える)
id: 42
event: price
data: {"symbol":"USDJPY","price":150.5}
retry: 3000
data: 3秒後に再接続してね各メッセージは空行で区切るのがポイント。id: を付けると、再接続時に Last-Event-ID ヘッダで送られます。
WebSocket(クライアント)
const ws = new WebSocket("wss://example.com/ws");
ws.addEventListener("open", () => ws.send(JSON.stringify({ type: "hello" })));
ws.addEventListener("message", (e) => console.log(JSON.parse(e.data)));
ws.addEventListener("close", (e) => console.log("closed", e.code, e.reason));WebSocket は最初だけ HTTP でハンドシェイクします。
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=用途別の選び方
- 単純な定期更新(管理画面のダッシュボードなど)→ ポーリング。実装が最も単純
- サーバーからの一方的な配信(通知・フィード・株価・ログ)→ SSE。HTTP の上で動き、再接続も標準。インフラ変更が要らない
- 双方向のリアルタイム(チャット・ゲーム・協調編集)→ WebSocket。全二重が要る場面
- 「WebSocket が要りそうで実は SSE で足りる」ケースは多い。受信は SSE・送信は通常の
fetch(POST)に分ければ、双方向を疑似的にまかなえてインフラが軽い
NOTE
迷ったらまず「本当に双方向が必要か」を問い直してください。サーバー→クライアントの配信が主目的なら、SSE のほうが実装・運用ともに軽く済むことが多いです。
よくある落とし穴
SSE
- HTTP/1.1 の接続数上限: ブラウザはドメインあたり最大6接続。複数タブで同一ドメインに繋ぐと枯渇する。HTTP/2 なら多重化され実質解消
- プロキシのバッファリング: Nginx は既定でレスポンスをバッファするため、イベントがまとめて届く(リアルタイムでなくなる)。
proxy_buffering offかX-Accel-Buffering: noで対処
WebSocket
- 認証でカスタムヘッダが使えない: ブラウザの WebSocket API は
Authorization等のヘッダを付けられない。代替は (1) Cookie(同一ドメインなら自動送信。Cookie と SameSite参照)、(2) クエリにトークン(URL に残るリスク)、(3) 接続後の最初のメッセージで認証 - スケールと sticky session: 長時間ステートフルな接続のため、ロードバランサで sticky が要りがち。状態は Redis 等の外部ストアへ逃がす
- ハートビート: 経路上のプロキシが無通信の接続を切ることがある。Ping/Pong で生存維持
Long polling
- ヘッダのオーバーヘッド: 毎回フルの HTTP ヘッダを往復するため、小さなペイロードでは割高
新顔: WebTransport
WebTransport(HTTP/3 / QUIC ベース)は、双方向・単方向ストリームとデータグラム(非信頼配信)を持ち、多重化で Head-of-Line ブロッキングを避けられます。MDN は「多くの用途で WebSocket を置き換えうる候補」と位置づけていますが、利用可能になったのは新しいブラウザ(2026年前後〜)で、旧環境は非対応です。低遅延ゲームなど先端用途で選択肢に入ります(普及はこれから)。
まとめ
- ポーリング=単純な定期更新、SSE=サーバーからの単方向配信、WebSocket=双方向リアルタイム
- SSE は HTTP の上で動き、自動再接続が標準。単方向で足りるならまず SSE
- WebSocket は強力だが、認証(カスタムヘッダ不可)・スケール(sticky/外部ストア)・ハートビートに注意
- SSE の HTTP/1.1 接続数上限とプロキシのバッファリングは定番のハマりどころ
- 先端用途には WebTransport(HTTP/3)も選択肢に
「双方向が本当に要るか」を最初に決めるだけで、選択はぐっと簡単になります。多くの配信系は SSE で十分で、双方向が必須のときだけ WebSocket、が実務的な指針です。


