WebSocketかSSEかポーリングか:リアルタイム機能の設計判断と落とし穴
リアルタイム機能の選び方を実務目線で。WebSocket vs SSE vs ポーリング、プレゼンス、再接続とハートビート、複数サーバーのpub/scaleまで僕の事故込みで解説。
「ここ、リアルタイムにしたいんですよね」
打ち合わせでそう言われて、僕は反射的に「じゃあWebSocketで」と答えました。で、その場のノリで作り始めて、3週間後に痛い目を見ました。やりたかったのは「管理画面の数字が10秒おきに更新される」だけ。WebSocketなんて要らなかったんです。常時接続を張った結果、サーバーを増やしたら配信が片方のサーバーにしか届かなくなり、原因究明に丸一日溶かしました。
「リアルタイム」という言葉は便利すぎて、中身がバラバラです。1秒以内に双方向で飛び交うチャットも、サーバーから一方的に流すだけの通知も、全部「リアルタイム」と呼ばれる。だから最初にやるべきは実装じゃなくて、どのくらいの即時性が、どっち向きに必要かを見極めることでした。
この記事は、その見極め方の話です。具体的なチャットアプリの実装(認証・room配信・テストクライアントまで動くコード)は姉妹記事 WebSocketチャットの実装 に置いたので、ここでは「何を選び、どこでスケールが詰まり、どこを自前で持つか」という設計判断に絞ります。
この記事の要点
- リアルタイムは3択(WebSocket / SSE / ポーリング)。双方向で高頻度ならWebSocket、サーバー→ブラウザの一方向ならSSE、数十秒に1回ならポーリングで十分。
- 「つながったら完成」は罠。スマホ回線は切れ、タブはスリープする。**再接続(指数バックオフ)とハートビート(死活監視)**を最初から設計に入れる。
- プレゼンス(オンライン表示)は接続イベントだけでは作れない。多重接続・離脱検知・最終接続時刻まで考えると一気に難しくなる。
- 詰まるのはたいていスケール。サーバーが2台以上になった瞬間、room配信はpub/sub(Redis等)なしでは届かない。
- 自前運用がつらくなったら Pusher / Ably / Supabase Realtime などのマネージドに逃がす。判断軸は接続数・配信保証・履歴・運用工数。
まず3択を選ぶ:WebSocket / SSE / ポーリング
リアルタイムの土台は、ざっくり3つしかありません。順番に見ます。
ポーリングは、ブラウザが一定間隔でサーバーに「変わった?」と聞きに行くだけ。仕組みが普通のHTTPなので、認証もキャッシュもログも既存のまま使えます。欠点は遅延と無駄打ち。10秒間隔なら最大10秒遅れるし、誰も更新していなくてもリクエストは飛び続けます。
**SSE(Server-Sent Events)**は、ブラウザが1本の接続を開いて、そこにサーバーが一方的にイベントを流し込む仕組みです。EventSource を使えばブラウザが自動で再接続まで面倒を見てくれる。サーバー→ブラウザの通知に限れば、WebSocketより圧倒的に楽です。ただしブラウザからの送信は普通のHTTPに分ける必要があります。
WebSocketは、1本の接続で双方向にメッセージを流せる仕組み。チャットや共同編集のように「ブラウザからも高頻度で送る」場合の本命です。代わりに、再接続・死活監視・配信スケールを全部自分で設計することになります。
迷ったときの判断表です。
| 用途 | 即時性 | 向き | 第一候補 | 理由 |
|---|---|---|---|---|
| チャット・共同編集・カーソル共有 | 1秒以内 | 双方向・高頻度 | WebSocket | ブラウザからも頻繁に送る |
| 進捗バー・株価・通知トースト | 1秒前後 | サーバー→ブラウザ | SSE | 受け取るだけ。再接続も自動 |
| 管理画面の集計・ジョブ状態 | 数十秒 | サーバー→ブラウザ | ポーリング | 仕組みが単純で運用が楽 |
| 課金・権限変更・履歴保存 | 確実性 | – | HTTP API | 届かない可能性のある経路に乗せない |
最後の行が、僕が一番伝えたいところです。WebSocketやSSEは「届けば便利」だが「必ず届く」保証はない。決済や権限変更や監査ログのような落とせない処理は、通常のHTTP APIとDB書き込みで確実に押さえる。リアルタイム配信は、あくまでその上に乗る「速報」だと考えると事故が減ります。
SSEとWebSocketの違いを一行で
よく聞かれるので先に。SSEは一方通行の自動ドア、WebSocketは双方向の通話回線です。サーバーからの通知だけならSSEで十分だし、再接続も標準で付いてくる。ブラウザからもリアルタイムに送りたくなった瞬間にWebSocketを検討する、という順番がきれいです。最初からWebSocketに飛びつくと、要らない複雑さを背負い込みます。
「つながったら完成」が一番危ない
ローカルで2タブ開いてメッセージが飛ぶと、完成した気になります。僕もなりました。でも本番の接続は、想像よりずっと汚い形で壊れます。
- スマホがWi-Fiからモバイル回線に切り替わって、接続が黙って死ぬ
- タブがバックグラウンドに回ってスリープし、復帰時には接続が切れている
- 遅い利用者の受信が追いつかず、送信キューがメモリを食う
- 誰かがコンソールから壊れたJSONや巨大ペイロードを投げてくる
これらに最低限備えるのが、再接続とハートビートです。
再接続は「即リトライ」が事故る
接続が切れたら即つなぎ直す——一見正しそうですが、これは危険です。サーバーが一瞬落ちたとき、全クライアントが同時に即リトライすると、復旧した瞬間に接続が殺到してまた落ちる。いわゆる接続嵐(thundering herd)です。
対策は指数バックオフ+ジッター。リトライ間隔を1秒→2秒→4秒…と倍々で広げ、さらに各クライアントに少しずつランダムなズレを足して、再接続のタイミングをばらけさせます。SSEの EventSource はこの再接続を内蔵していますが、WebSocketは自前です。
ハートビートは「死んだ接続」を掃除する仕組み
TCPは、ケーブルを抜いたような切断をすぐには教えてくれません。サーバーから見ると接続は「生きている」ように見えるのに、相手はもういない。これを放置すると、幽霊接続がメモリとroomメンバー数を汚し続けます。
そこで一定間隔でサーバーが ping を送り、pong が返らない接続を切る。これがハートビートです。ブラウザはプロトコルレベルの pong を自動で返すので、ブラウザのJavaScriptから ping を送る必要はありません(ここを誤解して二重実装しがちです)。詳しくは MDN WebSocket API と接続状態を表す readyState を確認してください。
プレゼンス(オンライン表示)が難しい本当の理由
「誰がオンラインか」を緑の丸で出す、よくあるやつ。接続したら「オンライン」、切れたら「オフライン」にすればいいと思いますよね。僕も最初そう作って、すぐ破綻しました。
難しさは3つあります。
- 同じ人が複数タブ・複数端末で繋ぐ。PCとスマホで2接続あるとき、PC側のタブを閉じても「オフライン」にしてはいけない。接続数で数える必要があります。
- 切断は遅れて分かる。電車でトンネルに入った人は、すぐにはオフライン判定されない。ハートビートの間隔ぶんだけ、表示がズレます。
- 最後にいた時刻を出したくなる。「3分前にオンライン」みたいな表示は、切断イベントの時刻をどこかに残しておかないと出せません。
設計としては、ユーザーIDごとに「今いくつ接続があるか」をカウントし、ゼロになったら一定の猶予(ハートビート1〜2回ぶん)を置いてからオフラインにする。最終接続時刻は別途記録する。これだけでだいぶ実用的になります。ちなみに後述のSupabase Realtimeなどマネージドは、このプレゼンスを機能として持っているので、自前で苦労する前に検討する価値があります。
本当の壁は「サーバーが2台目」になったとき
ここが冒頭で僕が溶かした丸一日の正体です。
1台のサーバーで動かしているうちは、roomメンバーは全員そのサーバーのメモリ上にいます。だから「room:demo に配信」はそのまま全員に届く。ところがアクセスが増えてサーバーを2台に増やすと、利用者AはサーバーX、利用者BはサーバーYに繋がる。Aがroomへ送ったメッセージはサーバーXのメンバーにしか届かず、Yにいるbにはまったく届きません。
[1台のとき] [2台にスケールしたとき]
サーバーX サーバーY
ユーザーA ─┐ ユーザーA ─┐ ┌─ ユーザーB
ユーザーB ─┼─ サーバー (Xのメモリ) (Yのメモリ)
ユーザーC ─┘ 送信 → Xの中だけ ✗ Yには届かない
解決策が pub/sub です。各サーバーはメッセージを直接配るのをやめ、いったん共有の仲介役(Redis Pub/Sub が定番)に「room:demo にこれ流して」と投げる。仲介役は全サーバーにそれを配り、各サーバーが自分の持つ接続にだけ届ける。これで何台に増えても配信が揃います。
注意点として、Redis Pub/Sub 自体は「今つながっている購読者」にしか配らず、メッセージを保存しません。サーバーが受け取り損ねた配信は消えます。だから繰り返しになりますが、落とせないイベントはDBにも書く。Redisを配信の道に使うときのTTLや無効化の考え方は Redisキャッシュの設計 も参考になります。
マネージドに逃がす判断(Pusher / Ably / Supabase Realtime)
ここまでを全部自前で持つのは、正直しんどいです。接続のスケール、pub/sub、プレゼンス、再接続、配信保証——どれも沼です。だから「いつ自前をやめてマネージドに払うか」を最初に線引きしておくと楽になります。
代表的な選択肢と性格はこんな感じです。
| 選択肢 | 性格 | 向いている場面 |
|---|---|---|
| 自前(Node + ws + Redis) | 何でもできるが全部自分の責任 | 小規模・要件が特殊・学習目的 |
| Supabase Realtime | Broadcast / Presence / DB変更購読をWebSocketで提供 | 既にSupabaseを使っている。プレゼンスを楽したい |
| Pusher | 手軽なマネージドWebSocket。導入が速い | とりあえず素早く本番に乗せたい |
| Ably | 配信保証・履歴・再送など堅牢 | 取りこぼせない通知、エンタープライズ要件 |
ざっくりした判断軸を僕の言葉で言うと、こうです。同時接続が数百を超えそう/配信の取りこぼしが許されない/メッセージの履歴・再送が要る——このどれかに当たったら、自前運用のコストよりマネージド料金のほうが安くつくことが多い。逆に、検証や小規模の社内ツールなら自前で十分です。Supabase Realtimeの仕様は Supabase Realtimeドキュメント、Phoenix Channelsベースのプレゼンス機能なども公式で確認できます。
コピペで動く:SSEで進捗を流す最小サーバー
設計判断の記事なので、ここでは「双方向じゃないならWebSocketは要らない」を体で分かってもらうコードを置きます。Claude Codeの長いタスクの進捗(解析中→テスト中→完了)をサーバーから一方的に流す、SSEの最小例です。Node.js標準モジュールだけで動き、ブラウザの自動再接続もそのまま効きます。
import { createServer } from "node:http";
const PORT = Number(process.env.PORT || 8090);
// SSE形式("data: ...\n\n")で1イベント送る小さなヘルパー
function sendEvent(res, payload) {
res.write(`data: ${JSON.stringify(payload)}\n\n`);
}
const server = createServer((req, res) => {
// ブラウザ側のテスト用ページ
if (req.url === "/") {
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(`<!doctype html><meta charset="utf-8">
<pre id="log"></pre>
<script>
// EventSourceは切断時に自動で再接続してくれる(WebSocketにはこれが無い)
const es = new EventSource("/progress");
es.onmessage = (e) => {
document.querySelector("#log").textContent += e.data + "\\n";
};
</script>`);
return;
}
// 進捗を流すSSEエンドポイント
if (req.url === "/progress") {
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
});
const steps = ["解析中", "テスト中", "差分確認中", "完了"];
let i = 0;
const timer = setInterval(() => {
if (i >= steps.length) {
sendEvent(res, { type: "done" });
clearInterval(timer);
res.end();
return;
}
sendEvent(res, { type: "progress", step: steps[i], at: new Date().toISOString() });
i += 1;
}, 1000);
// ブラウザが離脱したらタイマーを止める(幽霊処理を残さない)
req.on("close", () => clearInterval(timer));
return;
}
res.writeHead(404);
res.end();
});
server.listen(PORT, () => {
console.log(`SSE server on http://localhost:${PORT}`);
});
node sse-server.mjs で起動してブラウザで http://localhost:8090 を開くと、1秒ごとに進捗が流れて最後に止まります。これだけのことに双方向のWebSocketを使うのは過剰だ、というのが目で見て分かるはずです。ブラウザから操作を送り返す必要が出てきて初めて、WebSocketへ移ればいい。SSEの仕様は MDN Server-Sent Events にまとまっています。
Claude Codeに設計から相談するときのコツ
実装の前に、Claude Codeには「どの方式を選ぶべきか」から壁打ちさせると、後戻りが減ります。機能名だけでなく、即時性・向き・規模・落とせない処理を渡すのがコツです。Claude Codeの一般的な進め方は Claude Code common workflows にあります。
リアルタイム機能の方式を選びたい。実装より先に設計を相談したい。
要件:
- やりたいこと: チームのレビュー画面でコメントと既読を即時反映
- 即時性: 1秒以内に出てほしい
- 向き: 複数人が双方向に書き込む
- 規模: 同時接続は当面50、半年で500を想定
- 落とせない処理: コメント本文の保存(消えると困る)
出してほしいもの:
1. WebSocket / SSE / ポーリングのどれが妥当か、理由つきで
2. サーバーを増やしたときに配信が割れない構成(pub/subの要否)
3. 自前運用とマネージド(Pusher/Ably/Supabase)の損益分岐
4. 「保存」と「配信」をどう分けるかの線引き
生成された設計には、必ず「壊れ方」を逆質問させます。「再接続の嵐は起きないか」「サーバー2台目で配信が割れないか」「プレゼンスは多重接続を数えているか」。レビュー観点の整え方は コードレビューChecklist、認証まわりは セキュリティベストプラクティス が参考になります。
よくある質問
Q. SSEとWebSocket、結局どっちを使えばいい? A. サーバーからブラウザへ流すだけなら迷わずSSE。再接続が標準で付き、普通のHTTPなので運用が楽です。ブラウザからも高頻度で送る(チャット、共同編集)ならWebSocket。「双方向が要るか」が分かれ目です。
Q. ポーリングは時代遅れ? A. いいえ。数十秒に1回でいい管理画面の集計などでは、いまでも最適です。仕組みが単純で、既存の認証・キャッシュ・ログがそのまま効く。即時性が要らない場面で常時接続を張るほうが、むしろ過剰設計です。
Q. サーバーは1台のまま運用したら、pub/subは要らない? A. その通り、1台なら不要です。問題はスケールした瞬間に配信が割れること。将来2台以上にする可能性があるなら、最初からpub/sub(Redis等)を挟む前提で設計しておくと、移行で詰みません。
Q. プレゼンス(オンライン表示)を自前で作るのは大変? A. 多重接続のカウント、遅れて来る切断、最終接続時刻まで考えると、想像よりかなり大変です。Supabase RealtimeやAblyはプレゼンスを機能として持っているので、ここだけマネージドに任せる判断はよくあります。
Q. リアルタイム配信を監査ログ代わりにしていい? A. だめです。WebSocketもSSEもRedis Pub/Subも「届かないことがある」前提の経路です。課金・権限変更・重要操作の記録は、必ずDBやログ基盤へHTTP経由で別に残してください。
まとめ
リアルタイムは「とりあえずWebSocket」で始めると、たいてい遠回りします。先に決めるのは方式ではなく、どのくらいの即時性が、どっち向きに、どの規模で必要か。そこさえ固めれば、一方向ならSSE、数十秒ならポーリング、双方向高頻度ならWebSocket、と自然に選べます。
そしてWebSocketを選んだなら、再接続(指数バックオフ)、ハートビート、サーバー2台目で割れない配信(pub/sub)、落とせない処理のDB保存を、最初から一つのセットで設計する。プレゼンスや配信保証で消耗しそうなら、Pusher・Ably・Supabase Realtimeへ素直に逃がす。チャットの具体実装まで進む人は WebSocketチャットの実装 へ、HTTP側のイベント連携は Webhook実装ガイド へどうぞ。
僕がこの順番で設計をやり直してから、リアルタイム周りで丸一日溶かすことはなくなりました。チーム導入で方式選定や CLAUDE.md、障害時の切り戻しまで一緒に固めたい場合は Claude Code研修・相談 を、プロンプトやテンプレートで土台を整えたい場合は 教材一覧 を見てみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。