テックブログ

SSEとは?WebSocketとの違いとリアルタイム通知の実装

SSEとは?WebSocketとの違いとリアルタイム通知の実装

SSE(Server-Sent Events)とは、1本のHTTP接続を維持し、サーバーからブラウザへテキスト形式のイベントを継続的に送る仕組みです。通常のAPIのようにレスポンスを1回返して通信を終えるのではなく、新しい情報が発生するたびに同じ接続を使ってデータを配信します。WebSocketとは異なり、基本的にはサーバーからクライアントへの一方向通信であり、新着通知、処理進捗、ライブログ、ダッシュボードの更新などに向いています。

1. SSEとは

1-1. サーバーからブラウザへイベントを送る仕組み

SSEは、Server-Sent Eventsの略で、サーバー側で発生した情報をブラウザへ継続的に配信するためのWeb標準技術です。ブラウザがサーバーへ接続した後、そのHTTP接続を開いたままにし、サーバーが必要なタイミングでイベントを送信します。

通常のHTTP APIでは、ブラウザがリクエストを送り、サーバーがJSONなどのレスポンスを1回返すと通信が完了します。一方、SSEのレスポンスはすぐには完了せず、サーバーがイベントを書き込むたびに、ブラウザがその内容を順次受信します。

例えば、時間のかかるデータ集計を開始した後、進捗率が変わるたびにブラウザへ通知できます。ブラウザが「処理は終わりましたか」と何度も問い合わせる必要がないため、サーバーから情報を届けたい用途に適しています。

1-2. HTTP接続とtext/event-streamを利用する

SSEではHTTP接続を利用し、サーバーはレスポンスのContent-Typeにtext/event-streamを指定します。このメディアタイプによって、ブラウザはレスポンスを通常のHTMLやJSONではなく、継続的なイベントストリームとして扱います。

イベントはUTF-8のテキストとして送信され、フィールド名、コロン、値という形式で記述します。1つのイベントは空行で区切られ、ブラウザは空行まで受信した時点で、そのイベントをJavaScriptへ通知します。

SSEがHTTPを使うからといって、通常のAPIレスポンスと同じではありません。レスポンスを途中で終了せず、サーバー側でデータを少しずつ書き込み続ける必要があります。また、Webサーバーやプロキシがデータを一時保存するとリアルタイムに届かないため、バッファリング設定の確認も必要です。

1-3. 一方向通信が向いている用途

SSEは、サーバーからブラウザへの一方向通信に特化しています。サーバー側で発生したイベントをクライアントへ届ける用途では、通信モデルが単純で実装しやすいことが特徴です。

SSE接続を使って、ブラウザからサーバーへ自由にイベントを送り返すことはできません。ユーザー操作やフォーム入力などをサーバーへ送る場合は、fetch()によるPOSTリクエストなど、通常のHTTP APIを併用します。

新着通知、処理進捗、監視アラート、ニュース更新、ライブログなどはSSEに向いています。一方、チャットや対戦ゲームのように、クライアントとサーバーが頻繁にデータを送り合う用途ではWebSocketを検討します。

2. SSEの通信形式

2-1. dataフィールドでイベント内容を送る

SSEでは、イベントの本文をdataフィールドに記述します。data:の後ろに文字列を指定し、イベントの終わりには空行を入れます。

dataには通常の文字列だけでなく、JSONを文字列化した内容も格納できます。ただし、SSE自体がJSONを自動的に解析するわけではありません。受信したJavaScript側でJSON.parse()を実行する必要があります。

以下は、サーバーからブラウザへ進捗情報を送るイベントの例です。

data: {"progress":40,"status":"running"}

data: {"progress":80,"status":"running"}

data: {"progress":100,"status":"completed"}

コードの役割: dataフィールドを使用して、進捗率と処理状態をJSON形式の文字列として送信しています。各イベントの間に空行を入れることで、ブラウザが個別のイベントとして認識します。

実行時の注意点: 最後の空行が送信されるまで、ブラウザ側のイベント処理は開始されません。また、JSONに改行や特殊文字を含める場合は、サーバー側で正しくJSON文字列へ変換してください。

1つのイベント内でdataを複数行指定することもできます。

data: 1行目
data: 2行目
data: 3行目

複数のdata行は、ブラウザ側で改行を挟んだ1つの文字列として結合されます。長いテキストを複数行に分ける場合に利用できますが、JSONを送る場合は通常、1イベントにつき1つのJSON文字列にまとめると扱いやすくなります。

2-2. eventとidで種類や位置を管理する

SSEでは、eventフィールドでイベントの種類を指定し、idフィールドでイベントの識別子を設定できます。これにより、通知内容ごとに処理を分けたり、切断後にどの位置から再開するかを管理したりできます。

eventを省略したイベントは、ブラウザでmessageイベントとして処理されます。event: progressのように名前を指定した場合は、JavaScript側でaddEventListener("progress", ...)を使って受信します。

次の例では、進捗イベントと完了イベントに、それぞれイベント名とIDを付けています。

id: 101
event: progress
data: {"progress":50}

id: 102
event: complete
data: {"result":"success"}

コードの役割: eventによってイベントを種類別に分類し、idによって受信位置を識別できるようにしています。

実行時の注意点: IDは、単純な連番でもデータベース上のイベントIDでも構いません。ただし、再接続時にイベントを復元する場合は、サーバーがIDから続きのイベントを取得できる設計にする必要があります。

イベントIDは画面に表示するための番号ではなく、再開位置を管理するカーソルとして利用できます。複数台のサーバーで配信する場合でも同じIDを追跡できるよう、共有データベースやメッセージブローカーを使う設計が考えられます。

2-3. retryで再接続間隔を伝える

retryフィールドは、接続が切れた後、ブラウザが再接続を試みるまでの待機時間を伝えるために使います。値はミリ秒単位の整数で指定します。

EventSourceには自動再接続機能があり、ネットワーク切断や一時的なサーバー障害が発生すると、ブラウザは接続の回復を試みます。サーバーからretryを送ることで、再接続間隔を調整できます。

例えば、再接続まで5秒待たせたい場合は次のように送信します。

retry: 5000
data: 接続を開始しました

コードの役割: 再接続までの待機時間として5,000ミリ秒をブラウザへ伝えています。

実行時の注意点: retryには整数を指定します。不正な値は無視される可能性があります。また、短すぎる間隔を指定すると、障害発生時に大量の再接続が集中するため注意が必要です。

自動再接続があるからといって、切断対策が不要になるわけではありません。切断中のイベントを復元する仕組み、再接続の集中を避ける仕組み、認証期限が切れた場合の画面表示などは、アプリケーション側で設計する必要があります。

3. EventSourceを使った最小実装

3-1. ブラウザからSSE接続を開始する

ブラウザでは、標準APIであるEventSourceを使ってSSE接続を開始できます。接続先のURLをコンストラクターへ渡すだけで、サーバーとの接続が作成されます。

EventSourceは、SSE形式のレスポンスを解析し、受信したイベントをJavaScriptのイベントとして通知します。外部ライブラリを追加せずに利用でき、自動再接続も組み込まれています。

最小構成のJavaScriptは次のとおりです。

const source = new EventSource("/events");

source.onopen = () => {
  console.log("SSE接続が開始されました");
};

source.onmessage = (event) => {
  console.log("受信データ:", event.data);
};

コードの役割: /eventsへSSE接続を開始し、接続成功時と通常のメッセージ受信時に処理を実行します。サーバー側でeventを省略したイベントは、onmessageで受信できます。

実行時の注意点: /eventstext/event-streamを返す必要があります。通常のJSONレスポンスを返すAPIへ接続しても、SSEとして正しく動作しません。また、開発中に複数回コードを実行すると接続が重複することがあるため、既存接続の管理も必要です。

同一オリジンのURLでCookie認証を使用している場合は、通常、既存のCookieを利用して接続できます。異なるオリジンへ接続する場合は、CORSと認証情報の設定を追加で確認します。

3-2. messageイベントと名前付きイベントを受信する

eventフィールドが指定されていないSSEイベントは、messageイベントとして受信します。用途ごとにイベント名を指定した場合は、addEventListener()で名前付きイベントを受信します。

名前付きイベントを利用すると、進捗、完了、警告などの処理を分けて実装できます。すべてのイベントを1つのonmessageで判定するより、コードの目的が分かりやすくなります。

次の例では、通常メッセージ、進捗イベント、完了イベントを受信します。

const source = new EventSource("/events");

source.onmessage = (event) => {
  console.log("通常メッセージ:", event.data);
};

source.addEventListener("progress", (event) => {
  try {
    const payload = JSON.parse(event.data);

    const progressElement = document.querySelector("#progress");
    progressElement.textContent = `${payload.progress}%`;
  } catch (error) {
    console.error("進捗データの解析に失敗しました", error);
  }
});

source.addEventListener("complete", (event) => {
  const payload = JSON.parse(event.data);

  console.log("処理が完了しました:", payload);
  source.close();
});

コードの役割: progressイベントを受信して画面の進捗率を更新し、completeイベントを受信した時点でSSE接続を終了しています。

実行時の注意点: event.dataは常に文字列です。JSONとして利用する場合はJSON.parse()が必要です。サーバーから不正なJSONが届く可能性も考慮し、必要に応じて例外処理を実装してください。

完了後も通知を受け取り続ける画面では、source.close()を呼ばずに接続を維持します。一方、1つの処理の進捗だけを監視する画面では、完了時に明示的に接続を閉じると不要な接続を減らせます。

3-3. エラー処理と接続終了を実装する

EventSourceを利用する場合は、正常受信だけでなくエラー処理と接続終了も実装します。ネットワーク切断、サーバー停止、認証エラーなどが発生する可能性があるためです。

onerrorは、接続エラーや切断時に呼び出されます。ただし、一時的な切断ではEventSourceが自動再接続を行うため、onerrorが発生しただけで必ず接続を破棄する必要はありません。

次の例では、接続状態を確認しながらエラーを処理します。

const source = new EventSource("/events");

source.onerror = () => {
  switch (source.readyState) {
    case EventSource.CONNECTING:
      console.warn("SSEへ再接続しています");
      break;

    case EventSource.CLOSED:
      console.error("SSE接続が終了しました");
      break;

    default:
      console.error("SSE接続でエラーが発生しました");
  }
};

window.addEventListener("beforeunload", () => {
  source.close();
});

コードの役割: EventSourceのreadyStateを確認し、再接続中なのか、接続が終了したのかを判定しています。また、ページを離れる前に接続を閉じています。

実行時の注意点: onerrorは再接続可能な一時エラーでも発生します。エラーが1回発生しただけで毎回close()すると、自動再接続を利用できません。アプリケーションの要件に応じて、再接続を続ける条件と終了する条件を決めてください。

EventSourceの状態には、CONNECTINGOPENCLOSEDがあります。画面上に「接続中」「再接続中」「切断済み」と表示すると、利用者が現在の状態を理解しやすくなります。

サーバー側でSSEを送信する疑似コード

サーバー側では、レスポンスをtext/event-streamとして開始し、接続を閉じずにイベントを書き込み続けます。イベントごとに空行を付け、可能であれば書き込んだデータをすぐにクライアントへ送信します。

次の例は、特定のフレームワークに依存しない疑似コードです。

function handleSseRequest(request, response):
    response.status = 200
    response.setHeader("Content-Type", "text/event-stream")
    response.setHeader("Cache-Control", "no-cache")
    response.flushHeaders()

    eventId = 1

    while connectionIsOpen(request):
        progress = getCurrentProgress()

        response.write("id: " + eventId + "\n")
        response.write("event: progress\n")
        response.write(
            "data: " + toJson({
                "progress": progress
            }) + "\n\n"
        )

        response.flush()

        eventId = eventId + 1
        wait(1000)

    releaseResources()

コードの役割: SSE用のレスポンスを開始し、進捗情報を1秒ごとに名前付きイベントとして送信しています。ideventdataを順番に書き込み、空行でイベントを確定しています。

実行時の注意点: 実際のサーバーでは、接続ごとに処理スレッドを長時間占有しない非同期I/O方式が適している場合があります。また、クライアント切断時にはタイマー、購読処理、データベース接続などを確実に解放してください。

イベントがない時間が長く続く場合は、コメント行をハートビートとして送る方法があります。

: keep-alive

コロンから始まる行はコメントとして扱われ、JavaScriptにはイベントとして通知されません。プロキシのアイドルタイムアウトを避ける補助になりますが、インフラ設定そのものを不要にするものではありません。

4. WebSocketとの違い

4-1. 一方向通信と双方向通信

SSEとWebSocketの大きな違いは、通信方向です。SSEは基本的にサーバーからクライアントへの一方向通信、WebSocketはクライアントとサーバーの双方向通信を提供します。

SSEでは、ブラウザからサーバーへ情報を送る際に、通常のHTTP APIを併用します。WebSocketでは、接続後にクライアントとサーバーのどちらからでもメッセージを送信できます。

処理進捗や通知のように、主にサーバーが情報を配信する場合はSSEが適しています。チャット、共同編集、オンラインゲームなど、双方が頻繁に送信する場合はWebSocketが適しています。SSEはWebSocketの下位互換ではなく、通信要件が異なる別の選択肢です。

4-2. HTTP互換性と通信データ形式

SSEはHTTPレスポンスを利用し、text/event-stream形式のテキストを送信します。WebSocketは最初にHTTPで接続を開始した後、WebSocketプロトコルへ切り替えて通信します。

SSEで直接扱うデータはUTF-8のテキストです。JSONを文字列として送ることはできますが、画像や音声などのバイナリデータをそのまま効率よく送る用途には向きません。WebSocketではテキストとバイナリの両方を扱えます。

既存のHTTP認証、アクセスログ、ルーティング構成と組み合わせたい場合はSSEが導入しやすいことがあります。一方、低遅延な双方向通信やバイナリ通信が必要な場合は、WebSocketの特性が要件に合います。

4-3. 実装・運用コストから使い分ける

通信方式を選ぶ際は、機能だけでなく実装と運用のコストも比較します。通知だけが必要なシステムに双方向通信を導入すると、接続管理や再接続処理が必要以上に複雑になる場合があります。

EventSourceにはイベント形式の解析と自動再接続が組み込まれています。一方、ブラウザ標準のWebSocket APIには、切断後の自動再接続やメッセージ再送が標準機能として用意されていないため、アプリケーション側で設計する必要があります。

ただし、SSEでも配信サーバーのスケーリング、イベント履歴の保存、認証、プロキシ設定などは必要です。実装コードが短いことと、本番運用が自動的に簡単になることは同じではありません。

比較項目SSEWebSocketポーリング
主な通信方向サーバーからクライアント双方向クライアントから定期的に問い合わせ
接続方式HTTP接続を維持HTTP接続後にWebSocketへ切り替えリクエストごとにHTTP通信
主なデータ形式UTF-8テキストテキスト、バイナリJSON、HTML、バイナリなど
ブラウザAPIEventSourceWebSocketfetchなど
自動再接続EventSourceに組み込みアプリ側で実装次回リクエストとして実装
サーバーからの即時配信可能可能問い合わせ間隔に依存
向いている用途通知、進捗、ログ、更新情報チャット、ゲーム、共同編集更新頻度が低い情報、単純な状態確認
主な注意点一方向、接続維持、プロキシ設定再接続、状態管理、専用運用リクエスト増加、通知の遅延

どの方式にも利点と制約があります。通信方向、更新頻度、遅延要件、データ形式、既存インフラ、開発チームの運用経験を基準に選ぶことが重要です。

5. SSEが向いている場面

5-1. 通知や処理進捗の配信

SSEは、新着通知や長時間処理の進捗をブラウザへ配信する用途に向いています。サーバーで変化が発生したタイミングだけイベントを送れるためです。

ポーリングで進捗を確認する場合、ブラウザは処理状況が変わっていなくても定期的にリクエストを送ります。SSEでは接続を維持し、進捗が変わったときにサーバーから新しい値を配信できます。

例えば、動画変換、ファイル解析、AIによる文章生成、帳票作成、バッチ処理などの進捗表示に利用できます。ただし、処理開始の指示そのものはPOST APIで送り、その結果をSSEで受け取るように役割を分けるのが一般的です。

5-2. ログやダッシュボードのライブ更新

SSEは、ログや監視ダッシュボードをライブ更新する用途にも適しています。新しいデータが発生した順番に、サーバーからブラウザへ継続配信できます。

CI/CDの実行ログ、アプリケーション監視、売上集計、センサー情報などでは、画面全体を再読み込みせず、新しく到着した情報だけを追加表示できます。名前付きイベントを利用すれば、ログ、警告、メトリクスを別々に処理できます。

ただし、イベント発生量が非常に多い場合は、すべてをそのまま画面へ送るとブラウザの描画負荷が高くなります。サーバー側で一定時間ごとに集約する、ブラウザ側で表示件数を制限するなどの対策を検討します。

5-3. WebSocketやポーリングを選ぶべき場面

クライアントとサーバーが頻繁にデータを送り合う場合は、WebSocketを検討します。SSEはサーバーからブラウザへの配信に特化しており、双方向通信を1本の接続で実現するものではないためです。

更新頻度が低く、数十秒や数分の遅れが問題にならない場合は、ポーリングのほうが構成を単純にできることがあります。管理画面の状態確認などでは、無理に常時接続を維持しない判断も合理的です。

方式を選ぶ際は、「リアルタイム」という言葉だけで決めないことが重要です。誰がデータを送るのか、どの程度の遅延を許容できるのか、接続を常時維持する必要があるのかを整理して判断します。

6. 実装時の注意点

6-1. 自動再接続とLast-Event-IDを利用する

EventSourceには自動再接続機能がありますが、切断中に発生したイベントが自動的に復元されるわけではありません。確実に続きを配信するには、イベントIDとサーバー側の履歴管理を組み合わせます。

サーバーがidフィールドを送っていると、ブラウザは最後に受信したIDを保持します。再接続時には、その値がLast-Event-IDとしてサーバーへ伝えられるため、サーバーは次のイベントから配信を再開できます。

例えば、最後に受信したIDが102なら、サーバーは103以降を履歴から取得して送信します。ただし、同じイベントが再送される可能性も考慮し、画面側ではイベントIDを使って重複処理を避ける設計が有効です。

イベント履歴を永続化せず、メモリ上だけに保持している場合、サーバーの再起動によって再送できなくなります。イベントの欠落が許されないシステムでは、データベースやメッセージブローカーなど、再取得可能な保存先が必要です。

また、多数のクライアントが同時に再接続すると、サーバーへ負荷が集中することがあります。再接続間隔、サーバーの負荷分散、障害復旧時の接続集中まで含めて検証してください。

6-2. 認証とCookieの扱いを確認する

SSEで認証が必要な場合は、EventSourceの制約を理解したうえで認証方式を選びます。ブラウザ標準のEventSourceは、fetch()のように任意のHTTPヘッダーを自由に設定できません。

同一オリジンでCookieベースのセッション認証を使っている場合は、そのCookieを利用して接続できます。クロスオリジンで認証情報を送る場合は、withCredentialsとサーバー側のCORS設定を確認します。

const source = new EventSource(
  "https://api.example.com/events",
  {
    withCredentials: true
  }
);

コードの役割: 異なるオリジンのSSEエンドポイントへ、認証情報を含めて接続する設定例です。

実行時の注意点: サーバー側では、適切なAccess-Control-Allow-OriginAccess-Control-Allow-Credentialsの設定が必要です。許可元を無制限にせず、実際に接続するWebサイトのオリジンを指定してください。

アクセストークンをURLのクエリパラメーターへ付ける方法は、サーバーログ、アクセス解析、ブラウザ履歴などに値が残る可能性があります。安易に長期間有効なトークンをURLへ含めず、Cookie認証、短期間の接続用トークン、SSE対応ライブラリなどを要件に応じて検討します。

認証期限が切れた場合の画面動作も必要です。再接続を繰り返すだけでは利用者が原因を判断できないため、再ログインを促す表示や、通常APIで認証状態を確認する処理を用意します。

6-3. プロキシのバッファリングと接続数を確認する

SSEを本番環境で利用する場合は、アプリケーションコードだけでなく、リバースプロキシ、ロードバランサー、CDN、Webサーバーの設定を確認します。途中の機器がレスポンスをバッファリングすると、イベントがすぐにブラウザへ届かないことがあります。

開発環境では正常に動いていても、本番環境では複数イベントがまとめて到着することがあります。この場合は、プロキシのレスポンスバッファリング、圧縮、アイドルタイムアウト、キャッシュ設定などを確認します。

一定時間イベントが発生しない接続では、コメント形式のハートビートを定期的に送る方法があります。ただし、ハートビートだけですべてのタイムアウトを回避できるとは限らないため、ネットワーク機器側の設定と合わせて検証してください。

SSEは接続を長時間維持するため、接続数に応じてサーバーのファイルディスクリプター、メモリ、ワーカー、ロードバランサーの接続管理などへ影響します。利用できる接続数は、ブラウザ、HTTPのバージョン、サーバー構成、ネットワーク機器によって異なります。

HTTP/1.1とHTTP/2では、接続やストリームの管理方法も異なります。特定の最大接続数を前提に設計せず、実際に使用するブラウザと本番構成で負荷試験を行うことが重要です。

画面を閉じたときや処理が完了したときには、不要な接続を明示的に終了します。サーバー側でもクライアント切断を検知し、タイマー、イベント購読、ストリームなどのリソースを解放してください。

7. まとめ

7-1. SSEの特徴を整理する

SSEは、1本のHTTP接続を維持し、サーバーからブラウザへイベントを継続的に配信する仕組みです。レスポンスにはtext/event-streamを使用し、ブラウザでは標準のEventSource APIで受信できます。

通信方向は基本的にサーバーからクライアントへの一方向です。通知、処理進捗、ライブログ、ダッシュボード更新など、サーバー側で発生した変化を届ける用途に適しています。

自動再接続は便利ですが、イベントの復元、重複対策、認証、プロキシのバッファリング、接続数の管理まで自動的に解決するものではありません。実運用ではidLast-Event-ID、ハートビート、切断処理などを含めて設計します。

7-2. 通信方式を選ぶためのチェックリスト

通信方式は、SSE、WebSocket、ポーリングのどれが新しいかではなく、システムの要件に合っているかで選びます。まず、通信方向とリアルタイム性を整理してください。

サーバーからブラウザへ情報を届けることが中心ならSSE、双方向に頻繁な通信が必要ならWebSocket、更新頻度が低く多少の遅延を許容できるならポーリングが候補になります。

実装前には、次の項目を確認すると判断しやすくなります。

  • 情報を送るのは主にサーバー側か
  • クライアントからも頻繁にデータを送る必要があるか
  • テキストだけでなくバイナリデータを扱うか
  • 数秒の遅延を許容できるか
  • 切断中のイベントを復元する必要があるか
  • イベントの重複や欠落をどのように扱うか
  • Cookieやトークンを使った認証が必要か
  • クロスオリジン接続が必要か
  • プロキシやロードバランサーが長時間接続に対応しているか
  • 同時接続数を本番環境に近い条件で検証したか
  • 画面終了時や処理完了時に接続を閉じられるか

SSEはWebSocketを置き換えるための方式ではなく、サーバーからの継続配信をシンプルに実現するための選択肢です。それぞれの通信方式の特徴を理解し、必要な機能と運用条件に合う方法を選びましょう。

8. 参考リンク

この記事では、次の公式資料および仕様を参考にしています。