LabHub

ブログ

2026 年の realtime web — WebSocket・SSE・WebTransport・WebRTC 深掘り比較(LLM streaming が SSE を選んだ理由まで)

한국어English日本語

プロローグ — 「realtime」という言葉の罠

「realtime 機能を入れてほしい」という依頼が来る。最初に頭に浮かぶ言葉は WebSocket だ。とりあえず WS server を立て、router の後ろに sticky session を付け、token 認証を乗せる。半年後、同じ会社の別チームが LLM の token を stream していて — 不思議なことに — SSE を使っている。誰かが訊く: 「なんで WebSocket じゃないの?」

答えは単純ではない。「realtime」は単一の問題ではなく spectrum である。

それぞれに合う protocol が違う。この記事は 2026 年時点の 5 候補 — WebSocket、Server-Sent Events (SSE)、WebTransport、WebRTC DataChannel、long polling — を比較する。おまけに HTTP/2 Server Push がなぜ死んだか、LLM streaming のほぼ全部が SSE に収束した本当の理由まで。

TL;DR. 一方向 streaming なら SSE。双方向・低遅延なら WebSocket。game 系の unreliable multi-stream なら WebTransport。P2P や media なら WebRTC。そして — 正直に言うと — ただの polling で足りる場合も多い。


1 章 · 風景 — 2026 年の 5 つの protocol

        Browser ◀──────── Server (または別の browser)
        ───────────────────────────────────────────────
        WebSocket           双方向、TCP、frame 形式
        SSE (EventSource)   server から client への一方向、HTTP 上
        WebTransport        双方向、QUIC/HTTP3、多重化
        WebRTC DataChannel  P2P、UDP/SCTP、unreliable も可
        Long Polling        HTTP polling、response を保留

各 protocol を 1 行で:

そして死者: HTTP/2 Server Push. Chrome が 2022 年に無効化し、仕様からの削除が進行中。cache 動作が微妙で、実利益が小さかった。


2 章 · WebSocket — まだ働き者、まだ落とし穴

WebSocket は 2011 年に RFC 6455 として標準化された。要点は単純だ: HTTP/1.1 の Upgrade ヘッダで接続を開始し、以後は TCP 上で独自 frame (text / binary / Ping / Close) を双方向に流す。

頭の中の模型

        Client                          Server
          │  GET /ws HTTP/1.1               │
          │  Upgrade: websocket             │
          │ ──────────────────────────────▶ │
          │                                 │
          │ ◀────────────────────────────── │
          │  HTTP/1.1 101 Switching         │
          │                                 │
          │   ◀══════ frame ══════▶         │  (双方向)
          │   ◀══════ frame ══════▶         │
          │                                 │
          │   Close 0x88                    │

Upgrade handshake が終わるとその TCP 接続はもはや HTTP ではない。だから router・load balancer・proxy・CDN の挙動が微妙になる。

強み

落とし穴

  1. HTTP/2 ではない。 WS 自体は HTTP/1.1 の upgrade。RFC 8441 で HTTP/2 上 WebSocket が可能だが server 実装が一様でない。
  2. sticky session が必須。 WS 接続は同じ backend インスタンスに固定される必要があり、Pub/Sub のような fan-out 層が要る。
  3. head-of-line blocking。 1 本の TCP なので、1 つの大きな message が後続を塞ぐ。
  4. 自動 reconnect なし。 自分で書く — exponential backoff、再送 queue、sequence number。
  5. proxy 非互換。 一部の企業 proxy は WS handshake を切る。
  6. HTTP cache を回避。 GET ではないので CDN cache の恩恵なし。

小さな chat server (Node-ish + ws)

// server.ts — minimal WS echo + broadcast
import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({ port: 8080 })
const clients = new Set<WebSocket>()

wss.on('connection', (ws) => {
  clients.add(ws)
  ws.send(JSON.stringify({ type: 'welcome', count: clients.size }))

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString())
    for (const c of clients) {
      if (c !== ws && c.readyState === 1) {
        c.send(JSON.stringify({ ...msg, ts: Date.now() }))
      }
    }
  })

  ws.on('close', () => clients.delete(ws))
})

client 側:

const ws = new WebSocket('wss://example.com/ws')
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', name: 'Jun' }))
ws.onmessage = (e) => console.log('recv:', JSON.parse(e.data))
ws.onclose = () => {
  // reconnect ロジックは自分で書く — backoff、jitter、token refresh、...
}

これで終わりではない。production では — 認証・heartbeat・reconnect・message queue・rollout 時の graceful drain — まで含めて数百行に育つ。


3 章 · Server-Sent Events — LLM streaming が選んだ伏兵

SSE は 2009 年に HTML5 仕様に入った。あまりに単純で長らく軽く見られていたが、2024〜2026 年の LLM streaming brew によって 事実上の標準 になった。

頭の中の模型

        Client                                Server
          │ GET /stream HTTP/1.1                 │
          │ Accept: text/event-stream            │
          │ ──────────────────────────────────▶  │
          │                                      │
          │ ◀──────────────────────────────────  │
          │ HTTP/1.1 200 OK                      │
          │ Content-Type: text/event-stream      │
          │                                      │
          │ ◀── data: {"token":"hello"}\n\n     │
          │ ◀── data: {"token":" world"}\n\n    │
          │ ◀── data: [DONE]\n\n                 │
          │                                      │
          │   (response が閉じない)              │

接続はただの HTTP response。server は response を閉じずに data: ...\n\n 形式で 1 行ずつ流す。client は EventSource で自動 parse・自動 reconnect。

強み

弱み

なぜ LLM streaming が SSE を選んだか

OpenAI、Anthropic、Google Gemini、Cohere — ほぼ全ての LLM token streaming API が SSE。出典: OpenAI Streaming docs https://platform.openai.com/docs/api-reference/streaming、Anthropic Messages Streaming https://docs.anthropic.com/en/api/messages-streaming。理由は 7 つ:

  1. 本質的に一方向。 prompt は 1 回の POST、答えは token ごとに流れる。WS の双方向は不要。
  2. retry が自然。 通常の HTTP retry 方針 (429・5xx) がそのまま動く。
  3. 認証・rate limit・課金が標準 HTTP middleware に乗る。 WS は毎回自前。
  4. firewall・proxy 通過。 企業網で WS は塞がれることがある。
  5. CDN と相性。 Cloudflare・Vercel Edge Functions が SSE を一級でサポート。
  6. test が簡単。 curl -N で終わり。
  7. HTTP/2・HTTP/3 で多重化が無料。 同時 stream が 1 本の接続を共有。

WebSocket を使うとこの 7 つが全て自前項目になる。

30 行 token streamer (Bun / Node)

// app/api/chat/route.ts — 30 行 SSE token streamer
export const runtime = 'edge'

export async function POST(req: Request) {
  const { prompt } = await req.json()

  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder()
      const send = (data: unknown) => {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
      }

      // 仮想 LLM: 50 ms ごとに token を送る
      const tokens = ['こん', 'にちは', '、', ' SSE', ' demo', ' です', '。']
      for (const t of tokens) {
        send({ token: t })
        await new Promise((r) => setTimeout(r, 50))
      }
      send('[DONE]')
      controller.close()
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      Connection: 'keep-alive',
    },
  })
}

client 側 — EventSource は POST を受けないので、fetch streaming または SSE parser (例: eventsource-parser) を使う。

const res = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ prompt: 'hi' }),
  headers: { 'Content-Type': 'application/json' },
})
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buf = ''
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  buf += decoder.decode(value, { stream: true })
  for (const line of buf.split('\n\n')) {
    if (line.startsWith('data: ')) console.log(line.slice(6))
  }
  buf = ''
}

これが ChatGPT・Claude UI の token streaming の正体だ。40 行で終わる。


4 章 · WebTransport — HTTP/3 時代の新しい働き者

WebTransport は IETF が 2024 年に RFC 9484 として標準化した新 protocol。QUIC 上の双方向 + datagram を 1 本の接続で提供する。出典: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API

頭の中の模型

        Browser ──────── QUIC (UDP 上) ──────── Server
                ├── 双方向の信頼 stream #1
                ├── 双方向の信頼 stream #2
                ├── 単方向の信頼 stream #3
                └── unreliable datagram (UDP のように)

1 本の QUIC 接続上に複数 stream。ある stream の packet loss が他を塞がない — head-of-line blocking がない。WebSocket の最大の弱点が消える。

2026 年 browser サポート

server 実装: aioquic (Python)、quinn (Rust)、msquic (Microsoft)、Cloudflare Workers WebTransport API。出典: https://github.com/aiortc/aioquic

強み

弱み

datagram demo

// game 座標を 30 fps で unreliable datagram として送る
const transport = new WebTransport('https://game.example.com/wt')
await transport.ready

const writer = transport.datagrams.writable.getWriter()
const reader = transport.datagrams.readable.getReader()

setInterval(async () => {
  const buf = new ArrayBuffer(8)
  const view = new DataView(buf)
  view.setFloat32(0, player.x)
  view.setFloat32(4, player.y)
  await writer.write(new Uint8Array(buf))
}, 33)

;(async () => {
  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    const view = new DataView(value.buffer)
    other.x = view.getFloat32(0)
    other.y = view.getFloat32(4)
  }
})()

これを WebSocket で同じことをしようとすると、座標 1 packet が遅れたら後続が塞がる。WebTransport はただの新しい datagram。落ちたら落ちたまま、次へ。


5 章 · WebRTC DataChannel — P2P の王、運用の地獄

WebRTC は本来 video call 用に作られた。RTCDataChannel は同じ P2P 接続上に任意の data を流せる兄弟 API。

頭の中の模型

        Peer A                         Peer B
          │     (signaling server)       │
          │ ───── SDP offer ────────▶   │
          │ ◀──── SDP answer ─────────  │
          │ ───── ICE candidates ───▶   │
          │ ◀──── ICE candidates ─────  │
          │                              │
          │  ╔════ P2P SCTP/UDP ════╗   │
          │  ║   DataChannel        ║   │
          │  ║   Video/Audio        ║   │
          │  ╚══════════════════════╝   │

要点は signaling が別 であること。WebRTC は 2 peer の IP・port・media 能力を交渉する間、message を運ぶ channel が要る — それが signaling server (通常 WebSocket か SSE)。

強み

弱み

使いどころ

LLM token streaming に WebRTC を使う人はいない。その刃は用途が狭く、運用負担が重い。


6 章 · long polling — 古い友、死なず

browser は HTTP/1.1 の時代から polling をしてきた。その進化が long polling だ。client が GET を送ると、server は応答を 即座に返さず イベント発生まで保留する。イベントが起きたら応答し、client は即座に再度 GET。

頭の中の模型

        Client                        Server
          │ GET /events                  │
          │ ──────────────────────────▶  │
          │                              │ (event 待機、保留)
          │                              │
          │                              │ event 発生
          │ ◀────── 200 OK [event] ───── │
          │ GET /events?since=...        │
          │ ──────────────────────────▶  │
          │                              │ (また待機)

単純で、動かない環境がない。

強み

弱み

2026 年の現実

まだ生きている。特に — mobile SDK の fallback、社内 LAN chat、通知 system — で。SSE が使えない環境 (一部の embedded browser など) でも。「cool でない」と切り捨ててはいけない。


7 章 · 死者 — HTTP/2 Server Push の短い生涯

HTTP/2 Server Push は 2015 年に登場した。きれいな idea だった — client が要求する前に server がリソースを push、page が速くなる。

現実は失望だった。Chrome の telemetry (2020〜2021) では大多数の push が — 既に cache にあるので — 無駄だった。cache 挙動が微妙で、実装が一様でなかった。Chrome は 2022 年に無効化。出典: https://developer.chrome.com/blog/removing-push。仕様からも削除中。

代替。 真のリソース push なら 103 Early Hints + link rel=preload。message push なら SSE。


8 章 · 意思決定 matrix

use case第 1 候補代替備考
LLM token streamingSSEWSserver → client 一方向、retry / CDN friendly
chat (1:1、1:多)WebSocketSSE + POST双方向・低遅延
実時間 multiplayer gameWebTransportWebRTCunreliable datagram、no HoL blocking
dashboard / 実時間 metricsSSEWS一方向、自動 reconnect
協調 editor (CRDT)WebSocketWT双方向 + 順序
通話WebRTCmedia と data を 1 channel
P2P file 転送WebRTCWS relayserver 帯域節約
通知 (低頻度)long pollingSSEserver cost と複雑度を最小化
株 / coin ticker (broadcast)SSEWS一方向、fan-out が単純
IoT 命令 (双方向)WebSocketWT小さな payload、双方向
最終 fallbacklong pollingあらゆる環境を通る

この表は正解ではなく 出発点。決定にはトレードオフがあり、team の運用能力・既存 infra・user の環境が表を揺らす。


9 章 · 正直な意思決定 framework

5 つの質問を順に。

Q1. 真に双方向か、実は一方向か?

LLM の answer は 一方向。prompt は 1 回の POST、answer は token stream。chat message も実は — 送信は POST で完結、受信だけが stream — と見ると一方向。

真の双方向 は 2 方向で同時に message が流れる場合 — 協調 editor の CRDT 更新、multiplayer game の入力 / 状態、IoT の双方向命令。

一方向なら SSE をまず検討。cost・運用・複雑度が圧倒的に低い。

Q2. 信頼が必要か、unreliable でよいか?

chat message・決済 event・状態更新 — 信頼必須。TCP・QUIC stream。

game 座標・VOIP・実時間 sensor — unreliable OK。1 packet 落ちても次が追いつけば終わり。UDP・QUIC datagram。

unreliable が要るなら WebTransport (または WebRTC)。WS には unreliable mode がない。

Q3. P2P が本当に要るか?

P2P は — 本当に — server を経由しない場合だけ。通話、file 転送、真の P2P game。それ以外は client-server が圧倒的に単純。

P2P を決めたら STUN + TURN の基盤 cost・運用を受け入れる覚悟。TURN に fallback したらそれは実質 relay で、P2P の利点は消える。

Q4. 既存 infra は?

会社が既に Kafka + Pub/Sub + nginx で SSE を回しているなら — 新機能に WS を入れない。運用 team が両方を知らないといけなくなる。

WS を既にうまく運用していて、新機能に SSE を導入するなら理にかなう — 2 つの protocol が違う問題を解く場合に限る。

Q5. user の network 環境は?

企業網・キャリア網には — UDP 遮断、WS 遮断、idle response 遮断 — 何でもある。user の環境が多様なら fallback は必須。

        WT 試行 ──失敗──▶ WS 試行 ──失敗──▶ SSE 試行 ──失敗──▶ long polling

production の realtime stack はだいたいこの fallback chain を何らかの形で持つ。


10 章 · 実例 — 誰が何を使うか

パターンが見える — 一方向 streaming は SSE、双方向 chat は WS、media は WebRTC、game は段階的に WT


11 章 · 運用 note — 実際に痛い所

sticky session

WS・WT は client が同じ backend インスタンスに固定される。ALB・Envoy・HAProxy の sticky session 設定が要る。SSE は — response が最後まで 1 インスタンスで流れるが — reconnect ごとに別 インスタンスでも OK (server が stateless なら)。

graceful shutdown

deploy で WS 接続を切ると 1 万人の user が同時に再接続を試みて thundering herd。決まったパターン:

  1. 新規接続を拒否 (health check fail)。
  2. 既存接続に「すぐ切る」message を送る。
  3. client が jitter を入れて自発的に reconnect。
  4. 30〜60 秒後に強制 close。

SSE も同じ問題。long polling は — 1 request が短いので — 運がよい。

認証

WS の Upgrade handshake は HTTP なので cookie・header・token を送れる。ただし — 一部の library は handshake 後に認証を確認 — このパターンは危険。handshake で認証する。

WT は証明書ベース + header。SSE は — ただの HTTP なので — 最も単純。

WebRTC は signaling server で認証。P2P 接続自体は DTLS-SRTP で暗号化。

cost

大規模 (接続 10 万以上) では — 接続あたり memory — が決定的。Node WS は約 30〜50 KB / 接続、Go・Rust は 5〜15 KB。言語選択が運用 cost を左右する。

monitoring

OpenTelemetry のサポートは — WS 部分、SSE 完全、WT は 2026 年時点で進行中 — と異なる。


12 章 · 自作する WT datagram server (Node、aiortc なし)

browser 側 demo は上にあり、server は Rust の wtransport か Python の aioquic が最も一般的。Node では 2026 年 5 月現在、@fails-components/webtransport が最も成熟。

// server.ts — minimal WebTransport datagram echo
import { Http3Server } from '@fails-components/webtransport'
import { readFileSync } from 'node:fs'

const server = new Http3Server({
  port: 4433,
  host: '0.0.0.0',
  secret: 'demo',
  cert: readFileSync('./cert.pem'),
  privKey: readFileSync('./key.pem'),
})
server.startServer()

const stream = await server.sessionStream('/wt')
const reader = stream.getReader()
while (true) {
  const { value: session, done } = await reader.read()
  if (done) break

  await session.ready
  const dgReader = session.datagrams.readable.getReader()
  const dgWriter = session.datagrams.writable.getWriter()

  ;(async () => {
    while (true) {
      const { value, done } = await dgReader.read()
      if (done) break
      // echo
      await dgWriter.write(value)
    }
  })()
}

demo 用の証明書が要り (browser に自己署名を受けさせるには serverCertificateHashes の pin)、local test 用に Chrome flag を有効化。


13 章 · anti-pattern 10 個

  1. すべての realtime 要件に反射的に WebSocket。 一方向なら SSE が単純・安価。
  2. 認証を handshake 後に。 WS upgrade で認証しないと一瞬未認証の接続を受ける。
  3. 自動 reconnect を書かない。 mobile network で切断は日常。
  4. heartbeat なし。 idle proxy が 30 秒後に切るのに気づかない。
  5. message sequence ID なし。 reconnect 後の重複・欠落を検出できない。
  6. graceful shutdown なしで再 deploy。 自爆 DDoS の thundering herd。
  7. 巨大な単一 message。 WS の head-of-line blocking を自分で起こす。
  8. WebRTC を P2P でない用途に。 client-server なら WS・WT が単純。
  9. HTTP/2 Server Push を復活させようとする。 死者だ。
  10. fallback chain なし。 user の 30 % が企業 proxy の後ろにいるかもしれない。

14 章 · 2026 年予測と未来

LLM 時代以降の realtime web は — 1 つの protocol が全部を解くのではなく — use case ごとに最適 tool を選ぶ時代 だ。WS が全てを解いていた 2010 年代は終わった。


エピローグ — protocol は道具だ

この記事の 1 行要約: 「realtime」という言葉を見たらまず双方向か一方向かを問い、次に信頼 / unreliable を問い、次に P2P かを問う。3 つの問いと 4〜5 行で答えが出る。

LLM token streaming が SSE を選んだのは SSE が「最強」だからではない。その問題に対して SSE が最も単純な答え だからだ。同じ論理で、協調 editor は WebSocket が最も単純、game 座標は WebTransport が最も自然。

道具を愛するな、問題を愛せ。

14 項目 checklist

  1. 双方向は本当に必要か、実は一方向か?
  2. 信頼必須か、unreliable で良いか?
  3. P2P は本当に要るか?
  4. user の network 環境 (企業網・mobile・海外) を想定したか?
  5. fallback chain があるか?
  6. 認証を handshake で行うか?
  7. 自動 reconnect + backoff + jitter があるか?
  8. heartbeat / idle ping があるか?
  9. message sequence ID と resume 仕組みがあるか?
  10. graceful shutdown 手順があるか?
  11. sticky session / session affinity が router に設定されているか?
  12. 接続あたり memory cost を測ったか?
  13. monitoring / SLO が定義されているか?
  14. (LLM なら) SSE をまず検討したか?

anti-pattern 10 個 (要約)

  1. 一方向に WebSocket。
  2. 認証が遅い。
  3. reconnect なし。
  4. heartbeat なし。
  5. sequence ID なし。
  6. graceful shutdown なし。
  7. 巨大単一 message。
  8. WebRTC を P2P でない用途に。
  9. HTTP/2 Server Push 復活の試み。
  10. fallback chain なし。

次の記事予告

候補: edge runtime 上の SSE — Vercel / Cloudflare / Deno での LLM token streaming pattern 比較MoQ (Media over QUIC) 入門 — 次世代 live mediaCRDT + WebSocket — 協調 editor の運用 note

"realtime web の次の 10 年は 1 つの protocol ではなく — 問題に合う道具を選ぶ — 時代だ。一方向なら SSE、双方向なら WebSocket、unreliable multi-stream なら WebTransport、P2P なら WebRTC。そしてそれでも足りなければ long polling はまだ生きている。"

— 2026 realtime web 深掘り、終わり。


参考 / References

コメント

まだコメントはありません。

ログインするとコメントできます