「リアルタイムで更新したい」という要望に、反射的に WebSocket を選ぶ前に、
もっと簡単な選択肢がないかを確認します。
選択肢の比較
| 方式 | 向いている用途 | コスト |
|---|---|---|
| ポーリング | 数秒〜数十秒の遅延が許容できる | 最小 |
| Server-Sent Events | サーバー→クライアントの一方向 | 中 |
| WebSocket | 双方向・低遅延が必要 | 大 |
通知や進捗表示なら、ポーリングで十分なことがほとんどです。
setInterval(async () => {
const res = await fetch("/api/notifications/unread-count/");
updateBadge(await res.json());
}, 30000); // 30秒ごと
30秒に1回のポーリングで困る要件は、実務ではそれほど多くありません。
Server-Sent Events という中間解
サーバーからの一方向配信なら、WebSocket より簡単です。
通常のHTTPで動くため、プロキシ設定もそのまま使えます。
def stream(request):
def gen():
while True:
yield f"data: {json.dumps(get_progress())}\n\n"
time.sleep(1)
return StreamingHttpResponse(gen(), content_type="text/event-stream")
const es = new EventSource("/stream/");
es.onmessage = (e) => render(JSON.parse(e.data));
進捗バーやライブ更新はこれで足ります。自動再接続も標準で備わっています。
WebSocket が必要なケース
- チャット(双方向)
- 共同編集
- リアルタイム対戦
これらは WebSocket が適切です。ただし構成が変わります。
通常のHTTP: Nginx → Gunicorn(同期)
WebSocket : Nginx → ASGI サーバー + チャネル層(Redis 等)
運用対象が増えることを織り込んで判断します。
認可を忘れない
WebSocket 接続時の認可は、HTTPとは別に書く必要があります。
ここが抜けている実装をよく見ます。
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
user = self.scope["user"]
if not user.is_authenticated:
await self.close()
return
room = self.scope["url_route"]["kwargs"]["room"]
if not await self.can_join(user, room): # ← 部屋ごとの認可
await self.close()
return
await self.accept()
「ログインしていれば全部屋に入れる」状態になっていないか、必ず確認します。
再接続を前提に作る
モバイル回線では接続が頻繁に切れます。
function connect() {
const ws = new WebSocket(url);
ws.onclose = () => setTimeout(connect, backoff()); // 再接続
}
切断中に発生したメッセージをどう扱うかも決めておきます。
最後に受信したIDを送って差分を取得する形が一般的です。
まとめ
- まずポーリングで足りないかを検討する(30秒間隔で困る要件は少ない)
- 一方向配信なら SSE。自動再接続も標準で付く
- WebSocket は双方向が必要なときだけ。構成が増える
- 接続時の認可を必ず書く。部屋・リソース単位で確認する
- 再接続と、切断中のメッセージの扱いを設計に入れる