SSE (Server-Sent Events) とは - サーバーからのリアルタイム配信の仕組み
SSE はサーバーからクライアントへ HTTP 上で一方向のリアルタイムストリームを送る仕組み。WebSocket との違い・EventSource API の使い方・再接続処理を解説
SSE とは
SSE (Server-Sent Events) は、サーバーからクライアントへ一方向のデータストリームを HTTP のレスポンスとして送り続ける仕組みである。仕様は WHATWG HTML Standard の一部として定義されており、ブラウザ側の窓口は EventSource インターフェースになる。専用のプロトコルへ切り替えず、text/event-stream という MIME タイプのレスポンスを閉じずに書き足していくだけなので、HTTP/1.1 でも HTTP/2 でも同じコードが動く。
実利は、仕様が引き受ける範囲の広さにある。イベントの区切り方、文字コード、切断後の再接続、取りこぼした地点からの再開までがブラウザ実装側の責務として決まっているため、アプリケーションが書くのは送る内容だけで済む。
WebSocket との比較
選択の分岐は 1 つで、クライアントからサーバーへ随時送るものがあるかどうかである。無いなら SSE で足りる。
| 観点 | SSE | WebSocket |
|---|---|---|
| 方向 | サーバー → クライアント (一方向) | 双方向 |
| プロトコル | HTTP のまま (text/event-stream) | HTTP でハンドシェイクした後 ws:// へ切り替え |
| 自動再接続 | ブラウザが行う (後述の例外あり) | 自前で実装 |
| バイナリデータ | 非対応 (テキストのみ) | 対応 |
| 適するケース | 通知、ログ、株価 | チャット、ゲーム |
双方向が必要になった場合の選択肢は WebSocket API を参照。
text/event-stream の書式
ストリームの中身は行指向のテキストである。フィールド名: 値 の行を並べ、空行で 1 イベントの区切りとする。文字コードは UTF-8 に固定されている。
dataはイベントの本体。同じイベント内に複数行書くと、改行で連結された 1 つの文字列としてクライアントへ渡る。eventはイベント名。付けた場合はonmessageではなくaddEventListener('<イベント名>', ...)の側で受ける。idはこのイベントの識別子。ブラウザが記憶し、再接続時に送り返す。retryは再接続までの待ち時間 (ミリ秒)。- 行頭が
:の行はコメントとして無視される。接続を維持するためのキープアライブに使える。
フィールド名を書き間違えた行は例外にならず黙って捨てられる。データが届かないときは、まず curl でレスポンスの生テキストを覗き、空行区切りになっているかを確かめるのが早い。
クライアント側
受け取る側は EventSource に URL を渡すだけである。
const source = new EventSource('/api/notifications');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
source.onerror = () => {
// CONNECTING なら再接続待ち、CLOSED なら再接続は行われない
if (source.readyState === EventSource.CLOSED) {
console.log('接続は終了した');
}
};
// 不要になったら明示的に閉じる (閉じなければ再接続が続く)
// source.close();
onerror を「切れたが自動で復帰する」合図として扱うのは誤りである。復帰するかどうかは readyState を見ないと分からない。
再接続とイベントの取りこぼし
接続が切れたときの挙動も仕様に書かれている。ブラウザは待ち時間をおいて同じ URL へ再接続し、直前に受け取った id の値を Last-Event-ID リクエストヘッダーに載せて送る。サーバーはこの値を見て次のイベントから送り直せばよく、どこまで渡したかをクライアント側で管理する必要はない。
待ち時間の既定値は仕様が定めておらず実装依存で、実際には数秒程度になる。値を決めたい場合はストリームの中で retry フィールドを送る。
再接続されないケースは 2 つある。ステータスコードが 200 以外、または Content-Type が text/event-stream でない場合、ブラウザは接続の失敗として扱い再試行しない。サーバーが 204 No Content を返した場合は「もう送るものはない」と解釈され、こちらも再接続は止まる。配信を恒久的に打ち切りたいときはこの 204 を使う。
裏を返せば、間に挟まるプロキシが Content-Type を書き換えたり、認証切れで 401 が返ったりすると、SSE は静かに一度で終わる。切断が繰り返されるより気づきにくい壊れ方である。
サーバー側
サーバー側はヘッダーを立ててから、レスポンスを閉じずに書き続ける。
// Express での SSE
app.get('/api/notifications', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const send = (data: unknown) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
send({ type: 'connected' });
const interval = setInterval(() => {
send({ type: 'heartbeat', timestamp: Date.now() });
}, 30000);
req.on('close', () => clearInterval(interval));
});
Connection: keep-alive は HTTP/1.1 向けの指定で、HTTP/2 では不要である。一定間隔で何かを送っているのは、間に挟まる逆プロキシが無通信の接続を切るのを防ぐ意味もある。nginx を経由する構成では、応答をまとめてから流す挙動によって配信が止まって見えることがあり、この経路では X-Accel-Buffering: no を返して無効化する。
見落としやすい接続数の上限
SSE は接続を張ったまま維持するため、HTTP/1.1 の同時接続数の上限に正面からぶつかる。ブラウザが 1 つのドメインへ同時に張る接続は 6 本程度で、同じサイトのタブを 6 枚開くと 7 枚目以降は前のタブを閉じるまで待たされる。長く既知の制約として扱われてきた挙動であり、ブラウザ側の修正を待つ話ではない。
HTTP/2 では 1 本の接続を多重化するため、上限はサーバーが許す同時ストリーム数に置き換わる。仕様はこの値を 100 より小さくしないことを推奨しており、実装も概ねその水準にある。SSE を本番で使うなら HTTP/2 で配信するのが実質的な前提条件になる。厄介なのは表面化の仕方で、HTTP/1.1 のままの開発サーバーでは普通に動き、タブを複数開いた利用者だけが詰まる。
AWS Lambda での配信
Lambda はレスポンスストリーミングに対応しており、SSE の配信元にできる。関数 URL、InvokeWithResponseStream API、レスポンスの転送モードを指定した API Gateway のプロキシ統合のいずれかを入口にすると、レスポンスを書き足しながら返せる (2026 年 8 月時点)。
ただし制約は多い。
- ストリーミングをそのまま書けるのは Node.js のマネージドランタイムで、他の言語ではカスタムランタイムか Lambda Web Adapter を挟む。
- 応答の上限はストリーミングで 200 MB (まとめて返す通常の応答は 6 MB)。先頭 6 MB までは帯域の制限がなく、それを超えた分は 2 MBps に絞られる。
- 実行時間の上限は 900 秒 (15 分) で、これより長い接続は維持できない。
- クライアントが切断しても関数は止まらず、実行時間の全体が課金される。つなぎっぱなしの用途では、リクエスト数ではなく実行時間の合計がコストを決める。
- 関数 URL は VPC 内部からのアクセスに対応していない。
数分で終わるログの追い出しやジョブの進捗表示には十分だが、常時接続のダッシュボードには向かない。後者は AppSync のサブスクリプションや API Gateway の WebSocket API のように、接続の維持を専門に引き受ける仕組みへ寄せた方が安く済む。
適するユースケース
- ダッシュボードのリアルタイム更新
- ビルド/デプロイのログストリーミング
- 株価・為替のティッカー
- 通知フィード
いずれもクライアントから送るものがない用途である。判断の順序としては、まず双方向が必要かを問い、不要なら SSE を選ぶ。その上で、配信経路が HTTP/2 になっているか、id を振って再開できるようにしてあるか、恒久停止に 204 を使う設計になっているか、の 3 点を確認する。ここを押さえておけば、後から WebSocket へ乗り換える必要はほとんど生じない。
この記事は役に立ちましたか?
関連用語
WebSocket API
API Gateway の WebSocket API でサーバーレスなリアルタイム双方向通信を実現する仕組み
Fetch API
HTTP リクエストを送信するためのモダンな Web API で、XMLHttpRequest の後継
GraphQL Subscription
GraphQL でサーバーからクライアントへリアルタイムデータ更新を配信する仕組み
HTTP/3
QUIC の上で動作する HTTP のメジャーバージョン
WebSocket
クライアントとサーバー間で双方向のリアルタイム通信を実現するプロトコル
TCP 3-way ハンドシェイク
TCP 接続を確立するための 3 段階の手順で、SYN → SYN-ACK → ACK の順で行われる