SSE(Server-Sent Events)とWebSocketの違いは、ひと言でいえば「サーバーから流すだけのHTTPレスポンス」か「双方向に使える別プロトコルの接続」かです。通知やAIチャットの文字の逐次表示のように、サーバーから送るだけならSSEで足ります。チャットの送信やゲームの操作のように、クライアントからも高頻度に送るならWebSocketが選択肢です。本記事は、この2つの差を仕様とコードで示し、1件あたりの枠のバイト数と接続確立の往復回数で性能を比べたうえで、音声・映像向けのWebRTC、HTTP/3上のWebTransportを選ぶべき場面まで判断基準を整理します。
まとめ:SSEとWebSocketの違いと選び方の結論
| 技術 | 通信方向 | 下位の通信 | 向く用途 |
|---|---|---|---|
| SSE | サーバー→クライアント | HTTP(TCPまたはQUIC) | 通知・進捗・LLMの逐次出力 |
| WebSocket | 双方向 | TCP(HTTP/2・3は拡張CONNECT) | チャット・共同編集・対戦ゲーム |
| WebRTC | 双方向(P2P) | UDP(DTLS・SRTP・SCTP) | 音声・映像通話、画面共有 |
| WebTransport | 双方向 | HTTP/3(QUIC・UDP) | 低遅延で順序を問わないデータ |
- クライアントから送るのが「たまのリクエスト」だけなら、通常のHTTPリクエスト+SSEで組める。WebSocketは要らない。
- SSEはブラウザの
EventSourceが自動再接続と最終イベントID(Last-Event-ID)の通知まで行う。取りこぼし分の保存・再送はサーバー側で書く。WebSocketは再接続から自前で書く。 - 性能差が出るのは小さなメッセージを大量に送る場面だけ。枠はWebSocketが2〜14バイト、SSEは
idとeventを付けると27バイト前後になる。1秒に数件の通知なら差は無視できる。 - SSEの弱点は、HTTP/1.1での同時接続6本の上限、
EventSourceがGET専用でヘッダーを付けられないこと、テキスト専用であること。前の2つはHTTP/2とfetchで回避できる。 - 音声・映像はWebRTC。WebTransportは2026年3月にSafari 26.4が対応して主要ブラウザがそろったばかりなので、WebSocketの代替として採るなら縮退経路を残す。
本番で詰まりやすいのは、プロトコルの選択よりも中継機器の設定です。nginxのバッファリングとALBの60秒のアイドルタイムアウトは、SSEでもWebSocketでも最初に確認してください。
SSEとWebSocketの違い|仕様で比べる7項目と接続の仕組み
SSEはWHATWGのHTML Living Standard(9.2 Server-sent events)で定義された仕組みで、ブラウザ側のAPIは EventSource、サーバーは Content-Type: text/event-stream のレスポンスを閉じずに書き足していきます。WebSocketはRFC 6455で定義された独立したプロトコルで、HTTPのリクエストで始まり、HTTP/1.1ではサーバーの101応答後にWebSocketフレームへ切り替わります。HTTP/2・HTTP/3での通信経路は、拡張CONNECTのストリーム上です。
| 比較項目 | SSE | WebSocket |
|---|---|---|
| 通信方向 | サーバー→クライアントのみ | 双方向 |
| 接続の正体 | 継続するHTTPレスポンス | Upgradeまたは拡張CONNECT上のフレーム |
| 送れるデータ | UTF-8テキストのみ | テキストとバイナリ |
| 自動再接続 | あり(retry で間隔指定) |
なし(自前実装) |
| 取りこぼし分の再送 | ID通知は標準、保存・再送は実装が必要 | アプリで設計 |
| リクエストヘッダー | 任意指定不可(Cookieは条件付き) | 任意指定不可(サブプロトコル指定可) |
| HTTP/2・HTTP/3 | そのまま多重化される | RFC 8441・9220の拡張が必要 |
接続確立方式:HTTPレスポンスとUpgrade・拡張CONNECTの違い
SSEはただのHTTPレスポンスなので、認証・ログ・WAF・CDNなど既存のHTTP基盤がそのまま効きます。WebSocketは Upgrade: websocket ヘッダーで接続を乗り換えるため、途中のプロキシやロードバランサーには、アップグレードを通す設定が必要です。HTTP/2上でWebSocketを張るにはRFC 8441(2018年)のExtended CONNECT、HTTP/3ではRFC 9220(2022年)が必要で、ChromeとFirefoxは対応していますが、途中のロードバランサーやサーバーが対応していなければHTTP/1.1の別接続で張られます。WebSocketだけ別経路になる前提で、タイムアウトや台数の設計を分けておくのが安全です。
再接続と取りこぼし:Last-Event-IDは標準、WebSocketは自前で設計
SSEでは、サーバーが各イベントに id: を付けておくと、切断後にブラウザが自動で再接続し、最後に受け取ったIDを Last-Event-ID リクエストヘッダーで送ります。WHATWGの処理モデルにも、再接続のリクエストへこのヘッダーを載せる手順が書かれています。サーバーはその続きから送り直せばよく、再接続の間隔も retry: フィールド(ミリ秒)で指示することが可能です。WebSocketは切れたら close イベントが来るだけで、再接続の待ち時間、再送すべきメッセージの特定、重複の排除はアプリで設計します。Socket.IOのようなライブラリが使われる理由の多くはここにあります。
SSE・WebSocket・WebTransportの性能比較|枠のバイト数と往復回数
「どちらが速いか」は、1件ごとに付く枠(フレーミング)の大きさと、最初のデータが届くまでの往復回数に分けると比べられます。サーバーの処理速度やネットワークの帯域は、どのプロトコルでも同じ条件になるためです。
1メッセージあたりの枠:WebSocketは2〜14バイト、SSEは8〜27バイト
WebSocketのフレームヘッダーは、RFC 6455の5.2節で本文長に応じて2・4・10バイトと決まっており、クライアント→サーバー方向はマスク用に4バイトが加わります(最大14バイト)。SSEはフィールド名・コロン・改行がテキストで乗り、最小の data: x\n\n でも本文1バイトに8バイトが付きます。次のPythonは、両者の枠を仕様どおりに数える計算です。標準ライブラリだけで動きます。
# overhead.py:1メッセージあたりの枠のバイト数を仕様どおりに数える(python overhead.py)
import json
def ws_header(n, from_client):
# RFC 6455 5.2:本文長で 2 / 4 / 10 バイト、クライアント送信はマスク 4 バイト加算
base = 2 if n <= 125 else 4 if n <= 65535 else 10
return base + (4 if from_client else 0)
def sse_frame(payload, event_id=None, event=None):
# WHATWG HTML 9.2.5:フィールド名・コロン・空白・改行はすべてテキストで乗る
s = ''
if event_id is not None:
s += f'id: {event_id}\n'
if event is not None:
s += f'event: {event}\n'
s += f'data: {payload}\n\n'
return len(s.encode('utf-8'))
H2_DATA = 9 # RFC 9113 4.1:HTTP/2 フレームヘッダーは 9 バイト固定
for body in ['x', json.dumps({'id': 8, 'price': 108}, separators=(',', ':')), 'a' * 1000]:
n = len(body.encode('utf-8'))
sse_min = sse_frame(body) - n
sse_full = sse_frame(body, 8, 'price') - n
print(f'本文{n:>5}B | SSE最小 +{sse_min}B / id+event付き +{sse_full}B'
f'(HTTP/2なら +{H2_DATA}B)| WS 受信 +{ws_header(n, False)}B / 送信 +{ws_header(n, True)}B')
$ python overhead.py
本文 1B | SSE最小 +8B / id+event付き +27B(HTTP/2なら +9B)| WS 受信 +2B / 送信 +6B
本文 20B | SSE最小 +8B / id+event付き +27B(HTTP/2なら +9B)| WS 受信 +2B / 送信 +6B
本文 1000B | SSE最小 +8B / id+event付き +27B(HTTP/2なら +9B)| WS 受信 +4B / 送信 +8B
20バイトのJSONを流すと、SSE(id・event 付き)は27バイト、WebSocketは2バイトの枠が付きます。HTTP/2ではRFC 9113の9バイトのDATAフレームがさらに乗ります(RFC 8441でHTTP/2上に張ったWebSocketも同じ)。1秒に数回の通知ならこの差は無視できます。1秒に数十〜数百件の小さなメッセージを双方向に送る対戦ゲームや共同編集では、WebSocketのほうが通信の無駄を抑えられる方式です。逆に1KBを超える本文では、枠の差は数%以下に縮みます。
最初のデータが届くまでの往復回数:TCP・TLS・QUICの違い
新しく接続を張る場合、TCPの3ウェイハンドシェイクに1往復、TLS 1.3に1往復が掛かります。QUIC(RFC 9000)はトランスポートと暗号の確立を1往復にまとめるので、HTTP/3上の通信はここで1往復短くなります。
| 方式 | 新規接続から最初のデータまで | 既存の接続を再利用する場合 |
|---|---|---|
| SSE(HTTP/1.1・2) | TCP+TLS+リクエストで約3往復 | リクエストの1往復 |
| SSE(HTTP/3) | QUIC+リクエストで約2往復 | リクエストの1往復 |
| WebSocket(HTTP/1.1) | TCP+TLS+Upgradeで約3往復 | 再利用不可(専用接続) |
| WebSocket(RFC 8441) | HTTP/2の接続確立+CONNECT | 拡張CONNECTの1往復 |
| WebTransport | QUIC+CONNECTで約2往復 | CONNECTの1往復 |
| WebRTC | シグナリング+ICE+DTLSで数往復以上 | 同じピア間なら維持して使う |
往復回数が効くのは接続を張る瞬間だけで、張った後の1件あたりの遅延はどの方式もほぼ片道の伝送時間です。TCPベースのSSEとWebSocketで差が出るのは、パケットが失われたときの待ち(後述のヘッドオブラインブロッキング)です。ページを開くたびに配信を始めるダッシュボードなら、すでにページ本体で張ったHTTP/2の接続に相乗りできるSSEのほうが、最初の1件は早く届きます。
Node.js実装例:SSEの価格配信とWebSocketのエコー通信を1台で動かす
次のコードは、Node.js 22以降(2026年10月時点の最新は26.10系、LTSは24系)と、WebSocketライブラリwsの8.22系を前提に、1台のHTTPサーバーに両方の口を作る例です。SSEは標準の http モジュールだけで書け、WebSocketのサーバーはライブラリを使います。
// server.mjs(npm i ws の後、node server.mjs で起動)
import http from 'node:http';
import { WebSocketServer } from 'ws';
const server = http.createServer((req, res) => {
if (req.url === '/events') {
const lastId = Number(req.headers['last-event-id'] ?? 0);
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no',
});
res.write('retry: 3000\n\n');
let id = lastId;
const timer = setInterval(() => {
id += 1;
res.write(`id: ${id}\nevent: price\ndata: ${JSON.stringify({ id, price: 100 + id })}\n\n`);
if (id % 3 === 0) res.end(); // 3件ごとに切断して再接続を試す
}, 200);
const ping = setInterval(() => res.write(': ping\n\n'), 15000);
req.on('close', () => { clearInterval(timer); clearInterval(ping); });
return;
}
res.writeHead(404).end();
});
const wss = new WebSocketServer({ server, path: '/ws' });
wss.on('connection', (ws) => {
ws.on('message', (data, isBinary) => {
ws.send(isBinary ? data : `echo: ${data}`, { binary: isBinary });
});
});
server.listen(8080);
curl -N でSSEの口を叩くと、ストリームはただのテキストです。Last-Event-ID: 7 を付けて再接続すると、IDは8から再開します。この例は値をIDから計算しているだけなので、本番では送信済みイベントを保存しておき、受け取ったIDより後を送り直す処理が要ります。
$ curl -sN -H 'Last-Event-ID: 7' http://localhost:8080/events
retry: 3000
id: 8
event: price
data: {"id":8,"price":108}
id: 9
event: price
data: {"id":9,"price":109}
ブラウザ側は、SSEなら new EventSource('/events') に addEventListener('price', …) を付けるだけで、切断後の再接続とIDの引き継ぎはブラウザが行います。WebSocketは送受信の口が1本で、切断時の扱いは自分で書きます。Node.jsの WebSocket クライアントは公式ドキュメントのとおり22.0.0でフラグ不要になり、22.4.0で実験扱いを外れました。次のコードは追加のライブラリ無しで動きます。
// wsclient.mjs
const ws = new WebSocket('ws://localhost:8080/ws');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
ws.send('hello');
ws.send(new Uint8Array([1, 2, 3]));
};
ws.onmessage = (e) => {
console.log(typeof e.data === 'string' ? e.data : new Uint8Array(e.data));
if (e.data instanceof ArrayBuffer) ws.close(1000, 'done');
};
ws.onclose = (e) => console.log('closed', e.code, e.reason);
// 期待される出力: echo: hello、Uint8Array(3) [ 1, 2, 3 ]、closed 1000 done
WebSocketのクライアントには、再接続の処理がまだ1行も入っていません。本番ではここに指数バックオフ付きの再接続と、再接続後にどこから受け直すかを伝える仕組みが加わります(後述のコード)。SSEなら EventSource が肩代わりする部分で、サーバーから流すだけの用途にWebSocketを選ぶと、この差分がそのまま保守対象になります。
SSEのデメリットと回避策|接続数・ヘッダー・中継機器の3つの壁
SSEの弱点として語られるものは、HTTP/1.1時代の制約と、EventSource というAPIの制約が混ざっています。仕組みそのものの弱点は「一方向でテキスト専用」の1点だけです。
HTTP/1.1の同時接続6本の制限とHTTP/2による多重化
MDNのEventSourceのページは、HTTP/2を使わない場合、SSEの接続数はブラウザ×ドメインごとに6本までで、ChromeとFirefoxはこの問題を「Won’t fix」としていると説明しています。タブを7枚開くと7枚目の接続が待たされ、同じドメインへの通常のリクエストまで詰まります。HTTP/2では各側が相手の開始できる同時ストリーム数の上限(SETTINGS_MAX_CONCURRENT_STREAMS)を通知する仕組みです。RFC 9113上の初期値は無制限で、100以上が推奨されます。SSEはこの1本のTCP接続の上に他のリクエストと多重化されます。本番がHTTPS+HTTP/2で配信されていれば、この制約は実質的に問題になりません。開発環境の素のHTTP/1.1で再現して誤解しやすい点です。
EventSourceのGET専用・ヘッダー不可の制約とfetchによる回避
EventSource のコンストラクタが受け取るのはURLと withCredentials だけです。Authorization ヘッダーも、POSTの本文も付けられません。Cookie認証なら問題ありませんが、Bearerトークンで守るAPIや、プロンプトを本文で送るLLMのAPIには合いません。OpenAIのAPIは stream: true でSSE形式の応答を返しますが、公式のNode.js SDK(openai-node)は EventSource を使わず、POSTしたレスポンス本文を自前の解析処理(Stream.fromSSEResponse)で読んでいます。
回避策は、fetch で普通にリクエストを送り、レスポンス本文をストリームとして読むことです。空行(\n\n)でイベントを区切り、data: 行を取り出します。次のコードは上のサーバーが返すLF改行・JSONデータ専用の簡易版です。CRLF改行や複数行・非JSONのデータも受けるなら、仕様の解析手順に沿ったパーサー(npmのeventsource-parser、2026年10月時点で4.1系)を使ってください。
// fetchsse.mjs:ヘッダー付きでSSEを読む
const res = await fetch('http://localhost:8080/events', {
headers: { Authorization: 'Bearer dummy' },
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += value;
let i;
while ((i = buf.indexOf('\n\n')) >= 0) {
const block = buf.slice(0, i);
buf = buf.slice(i + 2);
const data = block.split('\n')
.filter((l) => l.startsWith('data:'))
.map((l) => l.slice(5).trimStart())
.join('\n');
if (data) console.log(JSON.parse(data));
}
}
// 期待される出力: { id: 1, price: 101 } … { id: 3, price: 103 }
代わりに失うのは自動再接続と Last-Event-ID の自動送信です。LLMの応答のように1回のリクエストで完結するストリームなら再接続は要らないので、この割り切りで十分です。
プロキシのバッファリングと60秒のアイドルタイムアウトで届かない
SSEがローカルでは流れるのに本番で届かない場合は、中継機器のレスポンスバッファリングを確認します。nginxのproxyモジュールは proxy_buffering が既定でonなので、SSEの口では proxy_buffering off; にするか、上のコードのようにレスポンスに X-Accel-Buffering: no を付けます。
もう1つはアイドルタイムアウトです。nginxの proxy_read_timeout とAWSのALBのアイドルタイムアウトは、どちらも既定60秒です。60秒以上イベントが無い配信は途中で切られます。WHATWGの仕様は、コロンで始まるコメント行(: ping)を15秒ほどごとに送る方法を挙げており、上のサーバーコードの ping がそれに当たります。WebSocketも同じ中継機器を通るので、ハートビートが要る点は変わりません。具体的な設定は次の章にまとめます。
nginx・ALB配下の設定例|SSEとWebSocketを同じドメインで通す手順
SSEとWebSocketは、同じドメインの別パスで受けると、Cookie認証やCORSの設定を1か所にまとめられます。リバースプロキシの仕組みと導入判断で扱うnginxを前段に置く場合、パスごとに必要な設定は違います。
nginxの設定:SSEはバッファ無効、WebSocketはUpgradeの中継
WebSocketはhop-by-hopヘッダーの Upgrade と Connection を明示的に引き継がないと、nginxで止まります。nginxのWebSocket proxyingの解説にある map の書き方を使うと、通常のリクエストとアップグレードを同じ設定で扱えます。
# /etc/nginx/conf.d/realtime.conf(server ブロックの中は抜粋)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream app {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
# SSE:バッファを切り、イベントが来ない時間より長いタイムアウトにする
location /events {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}
# WebSocket:Upgrade と Connection を中継する
location /ws {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 1h;
}
}
設定を変えたら nginx -t で文法を確かめ、nginx -s reload で反映します。SSEの口で Connection '' を指定しているのは、上流とのkeepaliveを効かせるためです。proxy_read_timeout 1h は上流から何も届かない時間の上限なので、15秒ごとのハートビートと組み合わせれば、実際に切れるのはクライアントが去ったときだけになります。
ALB・API Gateway・Cloudflareの時間上限と再接続前提の設計
AWSのALBは、ロードバランサー属性のドキュメントのとおりアイドルタイムアウトが既定60秒です。HTTP/2のPINGフレームはこのタイマーを戻さないと明記されているため、ハートビートはデータとして送ります。API GatewayのWebSocket APIのクォータは、アイドル10分・接続の最大2時間・フレーム32KB・メッセージ128KBです。CloudflareもWebSocketのドキュメントで、無通信の接続を閉じると明記し、クライアント側からのハートビートを勧めています。
上限の値はサービスごとに違いますが、結論は同じです。長時間の接続は「いつか切れる」前提で、クライアントが張り直し、どこから受け直すかをサーバーに伝える設計にします。ALBやNLBの振り分け方式の違いはロードバランサーの仕組みと振り分け方式を参照してください。
WebSocketを選ぶ条件と運用で増える作業|分散・上限・再接続
WebSocketを選ぶべきなのは、クライアントからの送信が高頻度で、かつ往復の遅延が体感に効く場合です。チャットの入力中表示、共同編集のカーソル同期、対戦ゲームの操作がこれに当たります。バイナリを低いオーバーヘッドで送りたい場合もWebSocketです。
その代わり、運用では次の作業が増えます。
- 水平分散:接続が特定のサーバーに張り付くので、別サーバーの利用者へ届けるにはRedisのPub/Subなどでサーバー間を中継する(SSEも同じ)。再送用にイベントをIDつきで残すなら、ValkeyとRedisのStreamsのようにIDで範囲を読める構造が合う。
- 接続時間の上限:前章のとおりAPI GatewayのWebSocket APIは接続の最大2時間で切断する。長時間接続は再接続前提で設計する。
- 送信の詰まり:ブラウザの
WebSocketAPIには受信側の処理が追いつかないときに送信を止める仕組み(バックプレッシャー)が無い。対応するWebSocketStreamはChrome 124以降などのChromium系ブラウザに限られ、MDNでは実験的かつ非標準の扱いで、FirefoxとSafariは未対応にとどまる。
再接続の実装:指数バックオフとジッター、イベントの再開位置の受け渡し
サーバーが落ちて全員が同時に切れたとき、全クライアントが同じ間隔で張り直すと、復旧直後のサーバーに接続が殺到します。待ち時間を倍々に伸ばし(指数バックオフ)、さらに乱数で散らす(ジッター)のが定石です。次のコードは、最後に受けたIDをクエリで渡し、サーバーがそれより後を送り直す前提のクライアントです。
// reconnect.mjs:指数バックオフ+フルジッターで張り直し、受信済みIDを渡す
let lastId = 0;
let attempt = 0;
function connect() {
const ws = new WebSocket(`ws://localhost:8080/ws?since=${lastId}`);
ws.onopen = () => { attempt = 0; };
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.id <= lastId) return; // 再送で重なった分は捨てる
lastId = msg.id;
console.log(msg);
};
ws.onclose = (e) => {
if (e.code === 1000) return; // 正常終了は張り直さない
const cap = Math.min(30000, 500 * 2 ** attempt); // 0.5秒から倍々、上限30秒
attempt += 1;
setTimeout(connect, Math.random() * cap);
};
}
connect();
SSEの EventSource は、この「張り直し」と「IDの受け渡し」をブラウザが行います。WebSocketでは上のおよそ20行と、サーバー側の since の解釈・イベント保存が、機能ごとに保守対象として残ります。
ハンドシェイク、フレーム構造、Origin検証まで含めた実装はWebSocketの仕組みとNode.jsでの実装・運用判断で扱っています。再接続やルームを自前で書きたくない場合は、WebSocketの上に独自プロトコルを載せたSocket.IOの仕組みとWebSocketとの違いも候補になります。
WebRTC・WebTransportとの違い|UDP系を選ぶべき場面と縮退の条件
SSEをHTTP/1.1・HTTP/2で、WebSocketをTCPベースの接続で使う場合は、1つのパケットが失われると後続のデータが再送を待たされます(ヘッドオブラインブロッキング)。映像や、最新の値だけが意味を持つゲームの位置情報では、この待ちが遅延の主因になります。これを避けるのがUDP系の2つです。
WebRTC:ブラウザ同士の音声・映像とP2Pデータチャネル
WebRTCはブラウザ同士を直接つなぎ、音声・映像をSRTPで、任意のデータをRTCDataChannel(DTLS上のSCTP)で送ります。RTCDataChannel は ordered: false や maxRetransmits: 0 を指定すると、順序や再送を諦めて遅延を優先する設定が可能です。ただし接続の確立にはシグナリング用のサーバーが別に要り、そのシグナリングにWebSocketがよく使われます。WebRTCはWebSocketの置き換えではなく、音声・映像を足すときに併用するものです。詳しくはWebRTCのP2Pの仕組みとICE・STUN・TURN、SFUの選定を参照してください。
WebTransport:HTTP/3上の複数ストリームとデータグラム
WebTransportは、QUICとHTTP/3の仕組みの上で、信頼性のあるストリームを複数本と、再送しないデータグラムを1本の接続で扱えるクライアントサーバー型のAPIです。仕様はW3CのWebTransportで策定が進んでいます。WebSocketの「1本の順序付き通信」と違い、ストリームごとに独立して流れるので、1つの遅れが他を止めません。
ブラウザ対応は、Chrome・Edge 97、Firefox 114に続き、2026年3月のSafari 26.4で主要ブラウザがそろい、MDNでもBaseline 2026(Newly available)の表示になりました。一方で、企業ネットワークではUDP 443が塞がれていることがあり、ロードバランサーやCDNの対応もTCPほど整っていません。WebSocketの代わりに採るなら、つながらないときにWebSocketへ戻る経路を必ず残してください。サーバー実装と縮退設計はWebTransportの仕組みと実装・採用判断にまとめています。
用途別の選定表|LLMストリーミング・通知・チャット・ゲームの第一候補
選定で迷ったら、「クライアントからの送信頻度」と「順序と再送が必要か」の2軸で決めます。
| 用途 | 第一候補 | 判断の理由 |
|---|---|---|
| LLMの応答の逐次表示 | SSE(fetchで読む) | 1回のPOSTに対する一方向の流れ |
| 通知・進捗バー・ダッシュボード | SSE | EventSourceが再接続・ID通知に対応 |
| チャット | WebSocket | 送信も受信も高頻度 |
| 共同編集・ホワイトボード | WebSocket | 小さな双方向メッセージが多い |
| ビデオ通話・画面共有 | WebRTC | メディアの低遅延とP2P |
| クラウドゲーミング・位置同期 | WebTransport | 古い値の再送を待たない |
| ツール呼び出しが多いAIエージェント | WebSocket | 接続を保ち差分の入力だけを送る |
| 音声対話AI | WebRTCまたはWebSocket | 提供元の対応方式に合わせる |
避けたいのは、「リアルタイムだからWebSocket」と決め打ちすることです。サーバーから流すだけの機能にWebSocketを使うと、EventSource なら不要な再接続処理を自前で抱えます。イベントの保存・再送やサーバー間の配信連携は、どちらを選んでも設計が必要です。逆に、SSEで作った通知に後から「既読の送信」が加わっただけなら、既読は通常のPOSTで送ればよく、WebSocketへ移る理由にはなりません。
実例として、AIエージェントと外部ツールをつなぐMCP(Model Context Protocol)は、2024-11-05版の常時接続のHTTP+SSE方式から、2025-03-26版のトランスポート仕様でPOSTへの応答をSSEで返せるStreamable HTTPへ移り、2026-07-28版ではサーバーから流すためのGETストリームも廃止しました。詳細はStreamable HTTPとMCPのSSEとの違いで解説しています。
業務システムの通知や進捗表示、社内チャット、AIの逐次応答を既存のWebアプリに組み込む場面では、どの画面で何秒ごとに何件流れるかを先に数えると、方式はほぼ自動的に決まります。要件の整理からプロトコル選定・実装・中継機器の設定まで外部に任せたい場合は、一創の業務用・Webアプリ開発でご相談を受け付けています。
よくある質問
SSEとWebSocketの比較で検索されることの多い質問に、本文の結論を短くまとめて答えます。
SSEとWebSocketの違いは何ですか?
SSEはサーバーからクライアントへ一方向に送る、終わらないHTTPレスポンスです。WebSocketはHTTPのリクエストから始めて張る双方向の接続で、バイナリも送れます。SSEを読むEventSourceには自動再接続と最終イベントIDの通知が標準で付きますが、保存・再送はサーバー側で実装します。WebSocketでは再接続についても自前での実装が必要です。
SSE(Server-Sent Events)のデメリットは?
一方向でテキスト専用であることが本質的な制約です。HTTP/1.1で同一ドメイン6本までの接続数制限と、EventSource がGET専用でヘッダーを付けられない点もよく挙がりますが、前者はHTTP/2で、後者はfetchでレスポンスを読むことで回避できます。中継機器のバッファリングとアイドルタイムアウトは、設定とハートビートで対処します。
SSEとWebSocketはどちらが速いですか?
張った後の1件あたりの遅延は、どちらもほぼ片道の伝送時間で大差ありません。差が出るのは枠の大きさで、20バイトのJSONならSSE(id・event付き)は27バイト、WebSocketは2バイトが付きます。1秒に数十件以上の小さなメッセージを送る場合はWebSocketが有利です。一方、ページ本体と同じHTTP/2接続に相乗りできるSSEは、最初の1件が届くまでの往復が少なく済みます。
WebSocketの代替技術は何がありますか?
サーバーから流すだけならSSE、音声・映像ならWebRTC、順序に縛られない低遅延のデータならWebTransportです。WebTransportは2026年3月に主要ブラウザがそろったばかりで、UDPが通らない環境向けにWebSocketへの縮退経路を残すのが前提になります。
ChatGPTのようなAIチャットはSSEとWebSocketのどちらを使いますか?
テキストの逐次表示はSSE形式のストリームが主流です。OpenAIのAPIも stream: true でSSE形式の応答を返します。一方、音声で対話するRealtime APIの接続方式はWebRTC・WebSocket・SIPです。テキストでも、モデルとツールの間を多数往復するエージェント処理向けに、Responses APIにはWebSocketモードがあります。1往復で終わる応答はSSE、往復が多い処理は双方向の接続という、送信頻度による使い分けです。
関連記事
- SSE(Server-Sent Events)とは|仕組み・WebSocketとの違い・実装を解説:SSE単体の仕組みと実装
- WebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説:WebSocketを深く実装する
- WebTransportとは?HTTP/3上の双方向通信の仕組みと実装・採用判断を解説:WebTransportの実装と縮退
- WebRTCとは?P2Pの仕組み・ICE/STUN/TURNからSFU選定・実装・運用判断まで実装者向けに解説:音声・映像を足すとき
- gRPCとWebSocketの違いは?双方向通信の仕組み・ブラウザ到達性・LB通過性で使い分ける:gRPCとの比較