Server-Sent Events (SSE) の使い方:完全ガイド (2026年版)
サーバーからクライアントへのリアルタイムストリーミングの解説
Hyperealで構築を始めよう
Kling、Flux、Sora、Veoなどに単一のAPIでアクセス。無料クレジットで開始、数百万規模まで拡張可能。
クレジットカード不要 • 10万人以上の開発者 • エンタープライズ対応
Server-Sent Events (SSE) の使い方:完全ガイド (2026)
Server-Sent Events (SSE) は、単一の HTTP 接続を介してサーバーからウェブクライアントへリアルタイムの更新をプッシュするための、シンプルで標準化された手法です。WebSockets とは異なり、SSE は単方向(サーバーからクライアントのみ)であり、プレーンな HTTP を使用し、特別な設定なしでプロキシやファイアウォールを通過でき、切断時には自動的に再接続されます。これにより、SSE はライブフィード、AI トークンのストリーミング、通知、ダッシュボードに理想的な選択肢となります。
SSE vs WebSockets vs Long Polling
実装に入る前に、どのような場合に SSE が適切な選択となるかを理解しておきましょう。
| 機能 | SSE | WebSocket | Long Polling |
|---|---|---|---|
| 方向 | サーバーからクライアント | 双方向 | サーバーからクライアント |
| プロトコル | HTTP | WS/WSS | HTTP |
| 自動再接続 | 標準搭載 | 手動 | 手動 |
| バイナリデータ | 不可 (テキストのみ) | 可能 | 可能 |
| ブラウザサポート | すべてのモダンブラウザ | すべてのモダンブラウザ | すべてのブラウザ |
| プロキシ対応 | 良好 | 問題が発生する場合がある | 良好 |
| 接続オーバーヘッド | 低 (単一 HTTP) | 低 (単一 TCP) | 高 (繰り返される HTTP) |
| 最適な用途 | フィード、通知、AI ストリーミング | チャット、ゲーム、共同編集 | レガシー互換性 |
SSE を使用すべきケース: サーバーからクライアントへの更新のみが必要で、自動再接続機能が必要であり、双方向通信よりもシンプルさを優先する場合。
SSE の仕組み
SSE プロトコルは非常にシンプルです。
- クライアントが
Accept: text/event-streamを含む標準的な HTTP GET リクエストを送信します。 - サーバーは
Content-Type: text/event-streamで応答し、接続を維持します。 - サーバーはイベントをプレーンテキストとして送信し、各イベントを2つの改行で区切ります。
- 接続が切断された場合、ブラウザは自動的に再接続を試みます。
SSE メッセージ形式
各 SSE メッセージは1つ以上のフィールドで構成され、各フィールドは個別の行に記述されます。
event: message_type
id: unique_id_123
retry: 5000
data: {"text": "Hello, world"}
フィールドの種類:
data:-- メッセージのペイロード(必須)。複数のdata:行は改行で結合されます。event:-- 名前付きイベントタイプ(任意)。デフォルトは"message"です。id:-- 一意のイベント ID(任意)。再接続時に使用されます。retry:-- ミリ秒単位の再接続間隔(任意)。
空行(2つの改行 \n\n)がイベントの終了を知らせます。
サーバーの実装
Node.js (Express)
const express = require("express");
const app = express();
app.get("/events", (req, res) => {
// SSE ヘッダーの設定
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
// ヘッダーを即座にフラッシュ
res.flushHeaders();
// 2秒ごとにイベントを送信
let counter = 0;
const interval = setInterval(() => {
counter++;
const data = JSON.stringify({
time: new Date().toISOString(),
count: counter
});
res.write(`id: ${counter}\n`);
res.write(`event: tick\n`);
res.write(`data: ${data}\n\n`);
}, 2000);
// クライアント切断時のクリーンアップ
req.on("close", () => {
clearInterval(interval);
res.end();
console.log("Client disconnected");
});
});
app.listen(3000, () => {
console.log("SSE server running on http://localhost:3000");
});
Python (FastAPI)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json
from datetime import datetime
app = FastAPI()
async def event_generator():
counter = 0
while True:
counter += 1
data = json.dumps({
"time": datetime.now().isoformat(),
"count": counter
})
yield f"id: {counter}\nevent: tick\ndata: {data}\n\n"
await asyncio.sleep(2)
@app.get("/events")
async def stream_events():
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
Python (Flask)
from flask import Flask, Response
import json
import time
from datetime import datetime
app = Flask(__name__)
def generate_events():
counter = 0
while True:
counter += 1
data = json.dumps({
"time": datetime.now().isoformat(),
"count": counter
})
yield f"id: {counter}\nevent: tick\ndata: {data}\n\n"
time.sleep(2)
@app.route("/events")
def stream():
return Response(
generate_events(),
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # nginx のバッファリングを無効化
}
)
クライアントの実装
ブラウザ (EventSource API)
ブラウザには、接続管理、自動再接続、およびイベント解析を処理する組み込みの EventSource API が用意されています。
// SSE エンドポイントに接続
const eventSource = new EventSource("http://localhost:3000/events");
// デフォルトの "message" イベントをリッスン
eventSource.onmessage = (event) => {
console.log("Message:", event.data);
};
// 名前付きイベントをリッスン
eventSource.addEventListener("tick", (event) => {
const data = JSON.parse(event.data);
console.log(`Tick #${data.count} at ${data.time}`);
});
// 接続開始時の処理
eventSource.onopen = () => {
console.log("Connection established");
};
// エラーと再接続の処理
eventSource.onerror = (error) => {
console.error("SSE error:", error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log("Connection was closed");
} else {
console.log("Reconnecting...");
}
};
// 完了時に接続を閉じる
// eventSource.close();
SSE 用 React フック
import { useEffect, useState, useCallback } from "react";
interface SSEOptions {
url: string;
eventName?: string;
onError?: (error: Event) => void;
}
function useSSE<T>(options: SSEOptions) {
const [data, setData] = useState<T | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(options.url);
eventSource.onopen = () => setIsConnected(true);
eventSource.onerror = (error) => {
setIsConnected(false);
options.onError?.(error);
};
const handler = (event: MessageEvent) => {
try {
const parsed = JSON.parse(event.data) as T;
setData(parsed);
} catch {
setData(event.data as unknown as T);
}
};
if (options.eventName) {
eventSource.addEventListener(options.eventName, handler);
} else {
eventSource.onmessage = handler;
}
return () => {
eventSource.close();
};
}, [options.url, options.eventName]);
return { data, isConnected };
}
// コンポーネントでの使用例
function LiveDashboard() {
const { data, isConnected } = useSSE<{ time: string; count: number }>({
url: "/events",
eventName: "tick",
});
return (
<div>
<p>Status: {isConnected ? "Connected" : "Reconnecting..."}</p>
{data && (
<p>Count: {data.count} | Time: {data.time}</p>
)}
</div>
);
}
実世界のユースケース:AI トークンストリーミング
2026年における SSE の最も一般的な用途の1つは、LLM API から AI 生成トークンをストリーミングすることです。実装方法は以下の通りです。
サーバー (AI API へのプロキシ)
app.post("/api/chat", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
try {
const aiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-4o",
messages: req.body.messages,
stream: true
})
});
const reader = aiResponse.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
// SSE データを直接転送
res.write(chunk);
}
res.write("data: [DONE]\n\n");
res.end();
} catch (error) {
res.write(`event: error\ndata: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
クライアント (fetch によるストリーミング)
POST リクエスト(EventSource は非対応)の場合は、Fetch API と Readable Stream を使用します。
async function streamChat(messages) {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") return;
try {
const parsed = JSON.parse(data);
const token = parsed.choices?.[0]?.delta?.content;
if (token) {
document.getElementById("output").textContent += token;
}
} catch {
// JSON 以外の行をスキップ
}
}
}
}
}
一般的な問題のトラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| イベントが一括で届く | プロキシのバッファリング (nginx, CloudFlare) | X-Accel-Buffering: no ヘッダーを追加。プロキシのバッファリングを無効化 |
| 60秒後に接続が切れる | サーバーまたはプロキシのタイムアウト | 30秒ごとにコメント行 (:keepalive\n\n) を送信 |
| 自動再接続されない | EventSource の代わりに fetch を使用している |
GET リクエストには EventSource を使用。POST の場合は手動リトライを実装 |
| CORS エラー | ヘッダーの不足 | サーバー側で Access-Control-Allow-Origin を追加 |
| 再接続時にイベントが重複する | イベント ID がない | id: フィールドを含める。サーバー側で Last-Event-ID ヘッダーを追跡 |
キープアライブ(Keep-Alive)パターン
プロキシのタイムアウトを防ぐために、定期的にコメント行を送信します。
// サーバー側のキープアライブ
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
req.on("close", () => {
clearInterval(keepAlive);
});
ベストプラクティス
- 常にイベント ID を含める: クライアントが再接続後に中断した場所から再開できるようにします。
- 適切なリトライ間隔を設定する:
retry:フィールドを使用します(デフォルトは通常3秒)。 - キープアライブ用のコメントを送信する: プロキシのタイムアウトを防ぐため、15〜30秒ごとに送信します。
- 名前付きイベントを使用する: 同一ストリーム内で異なる種類のメッセージを区別します。
- バックプレッシャーを処理する: 書き込み前にクライアントがまだ接続されているか確認します。
- 同時接続数を制限する: リソースの枯渇を避けるため、ユーザーごとの接続を制限します。
まとめ
Server-Sent Events は、サーバーからクライアントへのリアルタイム通信を実現するための、クリーンで標準に基づいたアプローチを提供します。単方向ストリーミングにおいては WebSockets よりもシンプルで、ブラウザ組み込みの EventSource API が再接続を自動的に処理してくれます。AI ストリーミング API の普及に伴い、SSE はすべての開発者にとって不可欠なツールとなりました。
AI 生成コンテンツ(リアルタイムの動画生成スタータス更新、プログレッシブ画像レンダリング、ライブ文字起こしフィードなど)をストリーミングするアプリケーションを構築しているなら、Hypereal AI が動画生成、AI アバター、画像合成のためのストリーミング対応 API を提供しており、生成プロセスの追跡用に SSE サポートが組み込まれています。
