開発・技術選定

リアルタイム機能を作る前に — WebSocket が本当に必要かの判断

「リアルタイムで更新したい」という要望に、反射的に 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 は双方向が必要なときだけ。構成が増える
  • 接続時の認可を必ず書く。部屋・リソース単位で確認する
  • 再接続と、切断中のメッセージの扱いを設計に入れる