LabHub

Blog

Realtime Web in 2026 — WebSocket vs SSE vs WebTransport vs WebRTC (and Why LLM Streaming Picked SSE)

한국어English日本語

Prologue — The Trap in the Word "Realtime"

You get a request: "add a realtime feature." The first word in your head is WebSocket. You spin up a WS server, sticky-session it behind your router, bolt on token auth. Six months later, another team in the same company is streaming LLM tokens — and, weirdly, they're using SSE. Someone asks: "Why not WebSocket?"

The answer isn't simple. "Realtime" is a spectrum, not a single problem.

Different problems want different protocols. This post compares the five candidates of 2026 — WebSocket, Server-Sent Events (SSE), WebTransport, WebRTC DataChannel, and Long Polling — plus a side note on why HTTP/2 Server Push died, and the real reason almost all LLM streaming converged on SSE.

TL;DR. One-way streaming: SSE. Bidirectional low latency: WebSocket. Game-style unreliable multi-stream: WebTransport. P2P or media: WebRTC. And — honestly — plain polling is often enough.


1. The Landscape — Five Protocols in 2026

        Browser ◀──────── Server (or another browser)
        ───────────────────────────────────────────────
        WebSocket           Bidirectional, TCP, framed
        SSE (EventSource)   Server-to-client one-way, over HTTP
        WebTransport        Bidirectional, QUIC/HTTP3, multiplexed
        WebRTC DataChannel  P2P, UDP/SCTP, optionally unreliable
        Long Polling        HTTP polling, response held open

One-sentence summaries:

And the dead one: HTTP/2 Server Push. Chrome disabled it in 2022 and the spec is removing it. Cache behavior was subtle, real-world wins were small.


2. WebSocket — Still the Workhorse, Still the Footguns

WebSocket was standardized as RFC 6455 in 2011. The core idea is simple: start with an HTTP/1.1 Upgrade handshake, then flip the TCP connection to a custom framing protocol (text, binary, ping, close).

Mental Model

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

Once the Upgrade handshake completes, that TCP connection is no longer HTTP. That's why routers, load balancers, proxies, and CDNs all behave a bit strangely with WS.

Strengths

Footguns

  1. It is not HTTP/2. WS itself is an HTTP/1.1 upgrade. RFC 8441 enables WebSocket over HTTP/2, but server support is uneven.
  2. Sticky sessions. A WS connection is pinned to one backend — you need a separate Pub/Sub fan-out layer.
  3. Head-of-line blocking. Single TCP — one large message blocks the rest.
  4. No automatic reconnect. You write that yourself — exponential backoff, retransmit queue, sequence numbers.
  5. Proxy-incompatible. Some corporate proxies kill the WS handshake.
  6. Bypasses HTTP caching. Not a GET, so no CDN cache benefit.

A Tiny 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 side:

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 = () => {
  // You write the reconnect logic — backoff, jitter, token refresh, ...
}

That's not the whole thing. Production grows into hundreds of lines: auth, heartbeats, reconnect, message queue, graceful drain during rollouts.


3. Server-Sent Events — The Surprise Winner of LLM Streaming

SSE entered the HTML5 spec in 2009. It looked so simple that for years people dismissed it — until the 2024-2026 LLM streaming boom made it the de facto standard.

Mental Model

        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                 │
          │                                      │
          │   (the response never closes)        │

The connection is just an HTTP response. The server keeps it open and emits one data: ...\n\n line per event. The browser's EventSource object parses and reconnects for you.

Strengths

Weaknesses

Why LLM Streaming Picked SSE

OpenAI, Anthropic, Google Gemini, Cohere — nearly every LLM token streaming API ships SSE. Sources: OpenAI Streaming docs https://platform.openai.com/docs/api-reference/streaming, Anthropic Messages Streaming https://docs.anthropic.com/en/api/messages-streaming. Seven reasons:

  1. The traffic is fundamentally one-way. The prompt is one POST; the answer is a stream. WS's bidirectionality isn't needed.
  2. Retries fit HTTP semantics. Normal 429/5xx policies just work.
  3. Auth, rate limiting, and billing slot into standard HTTP middleware. With WS you build all that yourself.
  4. Firewall and proxy compatibility. WS can be blocked in enterprise networks.
  5. CDN friendly. Cloudflare and Vercel Edge Functions treat SSE as a first-class shape.
  6. Easy to test. curl -N and you're done.
  7. Free multiplexing on HTTP/2 and HTTP/3. Concurrent streams share one connection.

If you use WebSocket, every one of those seven becomes something you have to build.

30-Line Token Streamer (Bun/Node)

// app/api/chat/route.ts — 30-line 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`))
      }

      // Fake LLM: emit a token every 50 ms
      const tokens = ['Hello', ',', ' SSE', ' demo', ' from', ' an', ' edge', ' function', '.']
      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',
    },
  })
}

The client — since EventSource can't POST — uses fetch streaming or an SSE parser like 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 = ''
}

That's the entire shape of ChatGPT- and Claude-style token streaming. Forty lines, end to end.


4. WebTransport — The HTTP/3 Era's New Workhorse

WebTransport is the IETF's RFC 9484, finalized in 2024. It puts bidirectional streams plus datagrams over QUIC on one connection. Source: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API.

Mental Model

        Browser ──────── QUIC (over UDP) ──────── Server
                ├── Bidirectional reliable stream #1
                ├── Bidirectional reliable stream #2
                ├── Unidirectional reliable stream #3
                └── Unreliable datagrams (UDP-like)

Many streams on one QUIC connection. Packet loss on one stream doesn't block the others — no head-of-line blocking. WebSocket's worst weakness disappears.

Browser Support, 2026

Server implementations: aioquic (Python), quinn (Rust), msquic (Microsoft), Cloudflare Workers WebTransport API. Source: https://github.com/aiortc/aioquic.

Strengths

Weaknesses

Datagram Demo

// Send game coordinates at 30 fps over unreliable datagrams
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)
  }
})()

Try this with WebSocket and one slow coordinate packet blocks the rest. WebTransport just drops the lost one and moves on.


5. WebRTC DataChannel — King of P2P, Hell to Operate

WebRTC was built for video calls. RTCDataChannel is a sibling API that lets you push arbitrary data on the same P2P connection as the media.

Mental Model

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

The key thing: signaling is separate. WebRTC needs an out-of-band channel to negotiate IPs, ports, and media capabilities — a signaling server, usually built on top of WebSocket or SSE.

Strengths

Weaknesses

When to Use It

Nobody uses WebRTC for LLM streaming. The knife is sharp but narrow, and operating it is expensive.


6. Long Polling — Old Friend, Refuses to Die

Browsers polled in the HTTP/1.1 era; Long Polling is the evolved version. The client sends a GET, the server doesn't close immediately, and instead holds the response until an event fires. Then it responds, and the client immediately reissues the GET.

Mental Model

        Client                        Server
          │ GET /events                  │
          │ ──────────────────────────▶  │
          │                              │ (wait for event, held open)
          │                              │
          │                              │ event fires
          │ ◀────── 200 OK [event] ───── │
          │ GET /events?since=...        │
          │ ──────────────────────────▶  │
          │                              │ (waits again)

Simple, and there's nowhere it doesn't run.

Strengths

Weaknesses

Where It Lives in 2026

Mobile SDK fallbacks. Internal LAN chat. Notification systems. Anywhere SSE can't go (some embedded browsers). Don't dismiss it because it isn't trendy.


7. The Dead — A Short Life for HTTP/2 Server Push

HTTP/2 Server Push arrived in 2015 with a clean idea: the server pre-pushes resources before the client requests them. Pages would feel faster.

Reality disappointed. Chrome telemetry (2020-2021) showed most pushes were wasted because resources were already cached. Cache behavior was subtle, implementations were inconsistent. Chrome disabled it in 2022. Source: https://developer.chrome.com/blog/removing-push. The spec is removing it.

Replacements. For resource hinting: 103 Early Hints plus link rel=preload. For messages: SSE.


8. Decision Matrix

Use caseFirst pickAlternativeNotes
LLM token streamingSSEWSOne-way server-to-client, retry- and CDN-friendly
Chat (1:1, 1:many)WebSocketSSE + POSTBidirectional, low latency
Realtime multiplayer gameWebTransportWebRTCUnreliable datagrams, no HoL blocking
Dashboards and live metricsSSEWSOne-way, auto reconnect
Collaborative editor (CRDT)WebSocketWTBidirectional + message ordering
Video callWebRTCMedia plus data on one channel
P2P file transferWebRTCWS relaySave server bandwidth
Notifications (low frequency)Long PollingSSEMinimal server cost and complexity
Stock and crypto tickers (broadcast)SSEWSOne-way, simple fan-out
IoT commands (bidirectional)WebSocketWTSmall payloads, both directions
Last-resort fallbackLong PollingWorks in any network

The table is a starting point, not a verdict. Every decision has tradeoffs, and your team's ops capability, existing infra, and users' network shape the answer.


9. An Honest Decision Framework

Ask these five questions in order.

Q1. Is the traffic truly bidirectional, or actually one-way?

LLM answers are one-way. The prompt is one POST; the answer is a token stream. Chat is mostly one-way too — sending is a single POST, receiving is the stream.

Truly bidirectional means messages flow both ways concurrently — CRDT updates in a collaborative editor, input and state in a multiplayer game, IoT command and response.

If it's one-way, look at SSE first. Cost, ops, and complexity are dramatically lower.

Q2. Reliable, or is unreliable acceptable?

Chat messages, payment events, state updates — reliability is mandatory. TCP, or QUIC reliable streams.

Game coordinates, VOIP, real-time sensors — unreliable is fine. A dropped packet doesn't matter; the next one catches up. UDP, or QUIC datagrams.

If you need unreliable, you need WebTransport or WebRTC. WS has no unreliable mode.

Q3. Do you really need P2P?

Only if you really mean it — no server in the data path. Video calls, file transfer, true P2P games. Everything else is dramatically simpler in client-server.

If you decide on P2P, accept the STUN and TURN infrastructure and ops. And if TURN takes over the relay, the P2P benefit is gone.

Q4. What infra do you already run?

If your company already runs Kafka, Pub/Sub, and nginx SSE well — don't bolt WS onto the new feature. Ops has to know both stacks.

If WS is already well-operated — adding SSE for a new product is fine if the problems are different.

Q5. What's the users' network?

Enterprise and carrier networks block all sorts of things — UDP, WS, idle responses. If your users live in varied networks, fallback is mandatory.

        Try WT ──fail──▶ Try WS ──fail──▶ Try SSE ──fail──▶ Long Polling

Most production realtime stacks have some version of this fallback chain.


10. Who Uses What in the Wild

The pattern is clear: one-way streaming is SSE, bidirectional chat is WS, media is WebRTC, gaming is incrementally WT.


11. Operational Notes — Where It Actually Hurts

Sticky sessions

WS and WT pin a client to one backend. Configure sticky sessions on your ALB, Envoy, or HAProxy. SSE is per-request — a response stays on one instance, but a reconnect can go anywhere as long as the server is stateless.

Graceful shutdown

If you drop 10,000 WS connections at deploy time, all 10,000 clients reconnect at once — thundering herd. The pattern:

  1. Reject new connections (fail health check).
  2. Send a "going down" message to existing connections.
  3. Let clients reconnect with jitter.
  4. Force-close after 30 to 60 seconds.

SSE has the same problem. Long Polling is luckier because each request is short.

Auth

The WS Upgrade handshake is HTTP, so cookies, headers, and tokens all work. Watch out for libraries that authenticate after the handshake — that's a window of unauthenticated connections. Auth at the handshake.

WT uses certs plus headers. SSE is straight HTTP — the simplest of all.

WebRTC authenticates at the signaling server. The P2P connection itself is encrypted with DTLS-SRTP.

Cost

At large scale (100k+ connections), memory per connection dominates. Node WS is roughly 30 to 50 KB per connection, Go and Rust are 5 to 15 KB. Language choice drives ops cost.

Monitoring

OpenTelemetry coverage varies — partial for WS, complete for SSE, in progress for WT as of 2026.


12. A Minimal WT Datagram Server (Node, without aiortc)

Browser demo is above; the server side most commonly uses Rust wtransport or Python aioquic. On Node in May 2026, @fails-components/webtransport is the maturest package.

// 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)
    }
  })()
}

You need a demo cert (or pin a self-signed cert via serverCertificateHashes in the browser), and Chrome flags enabled for local testing.


13. Ten Anti-Patterns

  1. Reflexively reaching for WebSocket for any realtime requirement. If it's one-way, SSE is simpler and cheaper.
  2. Authenticating after the handshake. A window of unauthenticated connections.
  3. Skipping automatic reconnect. Disconnects on mobile are constant.
  4. No heartbeat. Idle proxies drop the connection at 30 seconds and you don't know.
  5. No message sequence ID. After reconnect, you can't dedupe or replay.
  6. Redeploying without graceful shutdown. Thundering herd, self-inflicted DDoS.
  7. Giant single messages. Trigger your own head-of-line blocking.
  8. Using WebRTC for client-server. WS or WT is dramatically simpler.
  9. Reviving HTTP/2 Server Push. It is dead. Stop.
  10. No fallback chain. 30 percent of users might be behind a corporate proxy.

14. Predictions for 2026 and Beyond

The post-LLM realtime web isn't a single-protocol era. It's an era of picking the right tool per use case. The 2010s, when WS could solve everything, are over.


Epilogue — Protocols Are Tools

One sentence: the moment you see "realtime," ask first whether the traffic is one-way or bidirectional, then whether it needs to be reliable, then whether it must be P2P. Three questions and four or five lines later, you have an answer.

LLM token streaming picked SSE not because SSE is "the best" but because SSE is the simplest answer to that specific problem. By the same logic, collaborative editing is simplest on WebSocket, game coordinates are most natural on WebTransport.

Love the problem, not the tool.

14-Item Checklist

  1. Is the traffic truly bidirectional, or really one-way?
  2. Is reliability mandatory, or is unreliable acceptable?
  3. Is P2P actually needed?
  4. Did you model the users' network (enterprise, mobile, international)?
  5. Is there a fallback chain?
  6. Do you authenticate at the handshake?
  7. Do you have auto reconnect with backoff and jitter?
  8. Is there a heartbeat or idle ping?
  9. Are there message sequence IDs and resume semantics?
  10. Is there a graceful shutdown procedure?
  11. Are sticky sessions and session affinity configured at the router?
  12. Did you measure memory per connection?
  13. Are monitoring and SLOs defined?
  14. If it's LLM, did you consider SSE first?

Ten Anti-Patterns (Recap)

  1. WebSocket for one-way traffic.
  2. Late auth.
  3. No reconnect.
  4. No heartbeat.
  5. No sequence IDs.
  6. No graceful shutdown.
  7. Giant single messages.
  8. WebRTC for non-P2P.
  9. Reviving HTTP/2 Server Push.
  10. No fallback chain.

Next Up

Possible follow-ups: SSE on edge runtimes — comparing Vercel, Cloudflare, and Deno LLM streaming patterns, MoQ (Media over QUIC) — the next-generation live media protocol, CRDT plus WebSocket — operational notes for collaborative editing.

"The next decade of the realtime web is not one protocol — it's picking the right tool. One-way: SSE. Bidirectional: WebSocket. Unreliable multi-stream: WebTransport. P2P: WebRTC. And when none of that works — Long Polling is still alive."

— Realtime Web in 2026, end.


참고 / References

Comments

No comments yet.

Sign in to leave a comment