Skip to content

Split View: 2026년 실시간 웹 — WebSocket·SSE·WebTransport·WebRTC 심층 비교 (LLM 스트리밍이 SSE를 선택한 이유까지)

|

2026년 실시간 웹 — WebSocket·SSE·WebTransport·WebRTC 심층 비교 (LLM 스트리밍이 SSE를 선택한 이유까지)

프롤로그 — "실시간"이라는 단어의 함정

"실시간 기능을 넣어주세요"라는 요청을 받는다. 머리에 가장 먼저 떠오르는 단어는 WebSocket이다. 그래서 일단 WS 서버를 띄우고, 라우터 뒤에 sticky session을 붙이고, 토큰 인증을 얹는다. 6개월 뒤, 같은 회사의 다른 팀이 LLM 토큰을 스트리밍하는데 — 이상하게도 — SSE를 쓰고 있다. 한 명은 묻는다: "왜 WebSocket이 아니지?"

답은 단순하지 않다. "실시간"은 단일 문제가 아니라 스펙트럼이다.

  • 채팅 한 줄을 전 세계 1만 명에게 50 ms 안에 — 양방향 저지연.
  • LLM 토큰을 사용자 한 명에게 1분 동안 — 한 방향 스트림.
  • 게임 좌표를 1초에 60번, 패킷이 하나 빠져도 다음 패킷에 따라잡힘 — 다대다 비신뢰 데이터그램.
  • 화상 통화의 비디오/오디오 + 데이터 — P2P 미디어.
  • 5분에 한 번 알림을 폰에 — 그건 푸시지 실시간이 아니다.

각 문제에 맞는 프로토콜은 다르다. 이 글은 2026년 시점의 다섯 가지 후보 — WebSocket, Server-Sent Events(SSE), WebTransport, WebRTC DataChannel, Long Polling — 를 비교한다. 보너스로 HTTP/2 Server Push가 왜 죽었는지, 그리고 LLM 스트리밍이 거의 모두 SSE로 수렴한 진짜 이유까지.

TL;DR. 한 방향 스트리밍이면 SSE. 양방향 저지연이면 WebSocket. 게임처럼 비신뢰·다중 스트림이면 WebTransport. P2P 또는 미디어면 WebRTC. 그리고 — 정직하게 — "그냥 polling"으로 충분한 경우가 많다.


1장 · 풍경 — 2026년 다섯 가지 프로토콜

        브라우저 ◀──────── 서버 (또는 다른 브라우저)
        ─────────────────────────────────────────────
        WebSocket          양방향, TCP, 프레임 기반
        SSE (EventSource)  서버→클라 단방향, HTTP 위
        WebTransport       양방향, QUIC/HTTP3, 다중화
        WebRTC DataChannel P2P, UDP/SCTP, 비신뢰 가능
        Long Polling       HTTP 폴링, 응답 보류

각 프로토콜을 한 문장으로 요약하면:

  • WebSocket — 단일 TCP 연결 위에 양방향 프레임. 2011년 표준화, 가장 보편적, 가장 잘 알려진 함정도 가장 많음.
  • Server-Sent Events — HTTP 응답을 닫지 않고 텍스트 스트림을 흘려보냄. text/event-stream MIME, 자동 재연결 내장. 단방향(서버→클라).
  • WebTransport — QUIC 위의 양방향. 신뢰 스트림 + 비신뢰 데이터그램을 한 연결로. 헤드 오브 라인 블로킹 없음. Chrome 안정판에서 사용 가능.
  • WebRTC DataChannel — 두 피어 사이의 직접 연결. P2P, 옵션으로 비신뢰. 운영이 가장 어려움.
  • Long Polling — 옛 친구. 응답을 보류하다 이벤트 발생 시 응답. 모든 환경에서 동작.

그리고 죽은 사람: HTTP/2 Server Push. Chrome이 2022년에 비활성화했고, 명세에서 제거가 진행됐다. 캐시 동작이 미묘하고 실제 성능 이득이 적었다.


2장 · WebSocket — 여전히 일꾼, 여전히 함정

WebSocket은 2011년 RFC 6455로 표준화됐다. 핵심은 단순하다: HTTP/1.1 Upgrade 헤더로 연결을 시작해, 그 후엔 TCP 위에 자체 프레임(텍스트/바이너리/Ping/Close)을 양방향으로 흘린다.

머릿속 모델

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

Upgrade 핸드셰이크가 끝나면 그 TCP 연결은 더 이상 HTTP가 아니다. 그래서 라우터·로드밸런서·프록시·CDN의 동작이 미묘해진다.

강점

  • 진짜 양방향. 클라이언트가 언제든 보낼 수 있고, 서버도 언제든 보낼 수 있다.
  • 저지연. 핸드셰이크 한 번, 이후 프레임 오버헤드는 2~14 바이트.
  • 바이너리 안전. 게임·이진 프로토콜에 직접 쓸 수 있다.
  • 거의 모든 브라우저·언어·SDK 지원.

함정

  1. HTTP/2가 아니다. WS 자체는 HTTP/1.1 업그레이드다. RFC 8441로 HTTP/2 위 WebSocket이 가능하지만 서버 구현이 들쭉날쭉.
  2. 로드밸런서 sticky session. WS 연결은 같은 백엔드 인스턴스에 고정되어야 한다 — Pub/Sub 같은 fan-out 계층이 필요.
  3. 헤드 오브 라인 블로킹. 단일 TCP라, 한 메시지가 큰 페이로드면 그 뒤가 막힌다.
  4. 자동 재연결 없음. 직접 짜야 함 — exponential backoff, 메시지 재전송 큐, sequence number.
  5. 프록시 비호환. 일부 기업 프록시는 WS 핸드셰이크를 끊는다.
  6. HTTP 캐시 우회. GET이 아니므로 CDN 캐시 혜택 없음.

작은 채팅 서버 (Bun + ws 스타일)

// server.ts — 미니멀 WS 에코·브로드캐스트
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))
})

클라이언트 쪽:

const ws = new WebSocket('wss://example.com/ws')
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', name: '준' }))
ws.onmessage = (e) => console.log('recv:', JSON.parse(e.data))
ws.onclose = () => {
  // 직접 재연결 로직을 작성해야 한다 — 백오프, jitter, 인증 토큰 갱신…
}

이게 끝이 아니다. 프로덕션은 — 인증·하트비트·재연결·메시지 큐·롤아웃 시 graceful drain — 까지 합쳐 수백 줄로 자란다.


3장 · Server-Sent Events — LLM 스트리밍이 고른 우승자

SSE는 2009년에 HTML5 명세로 들어왔다. 너무 단순해서 처음엔 우습게 봤지만, 2024~2026년의 LLM 스트리밍 붐과 함께 사실상 표준이 됐다.

머릿속 모델

        Client                                Server
          │ GET /stream HTTP/1.1                 │
          │ Accept: text/event-stream            │
          │ ──────────────────────────────────▶  │
          │                                      │
          │ ◀──────────────────────────────────  │
          │ HTTP/1.1 200 OK                      │
          │ Content-Type: text/event-stream      │
          │                                      │
          │ ◀── data: {"token":"안녕"}\n\n      │
          │ ◀── data: {"token":"하세요"}\n\n    │
          │ ◀── data: [DONE]\n\n                 │
          │                                      │
          │  (응답이 영원히 안 닫힘)              │

연결은 그냥 HTTP 응답이다. 서버는 응답을 닫지 않고 data: ...\n\n 형식으로 한 줄씩 흘려보낸다. 클라이언트는 EventSource 객체로 자동 파싱·재연결한다.

강점

  • 단순하다. 그냥 HTTP다. 헤더, 메서드, 캐시 정책 그대로.
  • 자동 재연결. EventSource는 끊기면 자동으로 다시 연결, Last-Event-ID 헤더로 재개.
  • 방화벽 친화적. 일반 HTTP니까 어떤 프록시도 통과.
  • HTTP/2·HTTP/3 자동 사용. 다중화도 무료로.
  • 단방향 = 서버 로직이 단순. Pub/Sub fan-out에 자연스럽게 맞음.
  • CDN·gzip 호환. 응답이지 업그레이드가 아니라서.

약점

  • 단방향뿐. 클라이언트가 서버에 보내려면 별도 POST. (대부분의 LLM API가 정확히 이 패턴.)
  • 텍스트 전용. UTF-8 텍스트. 바이너리 보내려면 base64 — 비효율.
  • 연결 수 제한. HTTP/1.1에선 도메인당 6 — HTTP/2에선 거의 무한.
  • 타임아웃. 일부 프록시(특히 nginx 기본값)는 idle 응답을 끊는다 — 주기적 ping이 필요.

왜 LLM 스트리밍이 SSE를 골랐는가

OpenAI, Anthropic, Google Gemini, Cohere — 거의 모든 LLM 토큰 스트리밍 API는 SSE다. 자료: OpenAI Streaming docs https://platform.openai.com/docs/api-reference/streaming, Anthropic Messages Streaming https://docs.anthropic.com/en/api/messages-streaming. 이유는 일곱 가지:

  1. 본질적으로 단방향. 프롬프트는 한 번에 POST, 답은 토큰별로 흐른다. WS의 양방향이 필요 없다.
  2. 재시도가 자연스럽다. 보통의 HTTP 재시도 정책(429, 500, 5xx)이 그대로 작동.
  3. 인증·rate limit·과금이 표준 HTTP 미들웨어로 들어간다. WS는 매번 직접 짜야 함.
  4. 방화벽·프록시 통과. 기업망에서 WS는 막힐 수 있음.
  5. CDN과 호환. Cloudflare·Vercel Edge Functions가 SSE를 일급으로 지원.
  6. 테스트가 쉽다. curl -N이면 끝.
  7. HTTP/2·HTTP/3에서 멀티플렉싱이 공짜. 동시 스트림이 한 연결을 공유.

WebSocket을 쓰면 이 일곱 가지가 전부 직접 작성 항목이 된다.

30줄 토큰 스트리머 (Bun/Node)

// app/api/chat/route.ts — 30줄 SSE 토큰 스트리머
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마다 보냄
      const tokens = ['안녕', '하세요', ',', ' SSE', ' 데모', '입니다', '.']
      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',
    },
  })
}

클라이언트 — EventSource는 POST를 못 받으므로 fetch 스트리밍 또는 SSE 라이브러리(예: eventsource-parser)를 쓴다.

const res = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ prompt: '안녕' }),
  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 토큰 스트리밍의 모습이다. 40줄로 끝난다.


4장 · WebTransport — HTTP/3 시대의 새 일꾼

WebTransport는 IETF가 2024년 RFC 9484로 표준화한 새 프로토콜이다. QUIC 위의 양방향 + 데이터그램을 한 연결로 제공한다. 자료: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API.

머릿속 모델

        브라우저 ──────── QUIC (UDP 위) ──────── 서버
                ├── 양방향 신뢰 스트림 #1
                ├── 양방향 신뢰 스트림 #2
                ├── 단방향 신뢰 스트림 #3
                └── 비신뢰 데이터그램 (UDP-스럽게)

한 QUIC 연결 위에서 여러 스트림을 동시에. 한 스트림의 패킷 로스가 다른 스트림을 막지 않는다 — 헤드 오브 라인 블로킹이 없다. WebSocket의 가장 큰 약점이 사라진 셈.

2026년 브라우저 지원 현황

  • Chrome/Edge — 안정판 지원 (Chrome 97+, 2022년부터). https://chromestatus.com/feature/4854541929873408
  • Firefox — 안정판 지원 (Firefox 114+, 2023년부터). 기본 활성화는 작업 중.
  • Safari — 2026년 5월 현재 부분 지원(Safari Tech Preview에서 실험적). 안정판 전면 활성화는 아직.

서버 구현: aioquic(Python), quinn(Rust), msquic(MS), Cloudflare Workers WebTransport API. 자료: https://github.com/aiortc/aioquic.

강점

  • 헤드 오브 라인 블로킹 없음. 게임·다중 콘텐츠 스트림에 결정적.
  • 신뢰·비신뢰 둘 다. 비신뢰 데이터그램은 게임 좌표·VOIP 보조 트랙에 이상적.
  • 연결 마이그레이션. QUIC 특성 — 와이파이→셀룰러로 IP가 바뀌어도 연결 유지.
  • TLS 1.3 필수. 보안 기본.

약점

  • Safari 미완. 모든 사용자 대상이면 폴백 필요.
  • 운영 도구 미성숙. 디버깅 도구·로깅·메트릭 생태계가 WS만큼 풍부하지 않음.
  • UDP 차단. 기업망·통신사망 일부에서 UDP가 막혀 폴백이 필요.
  • 인증서 요구사항. 도메인 인증서가 필요(자체서명 도메인이면 serverCertificateHashes 옵션으로 우회 가능).

데이터그램 데모

// 게임 좌표를 30 fps로 비신뢰 데이터그램으로 보냄
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으로 똑같이 하면 — 좌표 한 패킷이 늦으면 그 뒤가 막힌다. WebTransport는 그냥 새 데이터그램이다. 잃어버리면 잃어버린 채로.


5장 · WebRTC DataChannel — P2P의 왕, 운영의 지옥

WebRTC는 본래 화상 통화용으로 만들어졌다. RTCDataChannel은 비디오/오디오 트랙과 같은 P2P 연결 위에 임의 데이터를 흘릴 수 있게 해주는 보조 API다.

머릿속 모델

        Peer A                         Peer B
          │       (시그널링 서버)        │
          │ ───── SDP offer ────────▶   │
          │ ◀──── SDP answer ─────────  │
          │ ───── ICE candidates ───▶   │
          │ ◀──── ICE candidates ─────  │
          │                              │
          │  ╔════ P2P SCTP/UDP ════╗   │
          │  ║   DataChannel        ║   │
          │  ║   Video/Audio        ║   │
          │  ╚══════════════════════╝   │

핵심은 시그널링은 별도라는 점이다. WebRTC는 두 피어의 IP·포트·미디어 능력을 협상하는 동안 메시지를 전달할 채널이 필요하다 — 그게 시그널링 서버다(보통 WebSocket이나 SSE로 구현).

강점

  • 진짜 P2P. 서버를 경유하지 않으니 지연이 가장 낮고, 서버 대역폭이 들지 않음.
  • 비신뢰 모드. ordered: false, maxRetransmits: 0으로 UDP스럽게.
  • NAT 통과. STUN·TURN 인프라가 NAT 뒤의 사용자도 연결.
  • 미디어와 일체. 화상통화에 데이터 채널을 한 연결에 얹을 수 있음.

약점

  • 운영이 가장 어렵다. 시그널링 서버 + STUN 서버 + TURN 서버(NAT 통과 실패 시 폴백) — 세 가지 인프라.
  • TURN은 P2P가 아니다. TURN으로 폴백하면 사실상 서버 릴레이고, 트래픽·과금이 폭발.
  • 연결 시간이 길다. 수백 ms~수 초의 ICE 협상.
  • 브라우저 호환성의 미세 차이. Chrome/Firefox/Safari가 모서리에서 다르게 동작.

언제 쓰나

  • 화상/음성 통화에 부가 데이터(채팅·파일 전송·게임 입력).
  • 진짜 P2P 게임(서버 비용 절감, 지연 최소화).
  • 파일 공유(WebTorrent류).
  • 메타버스·VR — 위치·표정 등 고빈도 데이터.

LLM 토큰 스트리밍에 WebRTC를 쓰는 사람은 없다. 그 칼은 사용처가 좁고 운영 부담이 크다.


6장 · Long Polling — 옛 친구, 죽지 않았다

브라우저는 HTTP/1.1 시절부터 polling을 했고, 그게 진화한 게 Long Polling이다. 클라가 GET을 보내면 서버는 응답을 즉시 닫지 않고 이벤트가 생길 때까지 보류한다. 이벤트가 생기면 응답하고, 클라는 즉시 다시 GET.

머릿속 모델

        Client                        Server
          │ GET /events                  │
          │ ──────────────────────────▶  │
          │                              │ (이벤트 대기, 보류)
          │                              │
          │                              │ 이벤트 발생!
          │ ◀────── 200 OK [event] ───── │
          │ GET /events?since=...        │
          │ ──────────────────────────▶  │
          │                              │ (또 대기)

지금 봐도 단순하고, 동작 안 하는 환경이 없다.

강점

  • 모든 환경에서 동작. HTTP/1.0이라도. 어떤 프록시도 통과.
  • 인증·CORS·캐시·CDN 모두 표준 HTTP.
  • 상태가 적다. 한 요청-한 응답이라 서버가 stateless로 갈 수 있음.
  • fallback으로 최고. WS·SSE·WT 모두 실패해도 polling은 동작.

약점

  • 요청 오버헤드. 매 이벤트마다 HTTP 헤더 비용.
  • 지연이 변동적. 응답 후 다음 GET 사이에 갭이 있음.
  • 타임아웃 균형. 너무 길면 프록시가 끊고, 너무 짧으면 polling이 됨.

2026년 현실

여전히 살아 있다. 특히 — 모바일 SDK 폴백, 사내 LAN 채팅, 알림 시스템 — 에서. 그리고 SSE를 못 쓰는 환경(IE는 죽었지만 일부 임베디드 브라우저)에서. "쿨"하지 않다고 무시하면 안 된다.


7장 · 죽은 자 — HTTP/2 Server Push의 짧은 생애

HTTP/2 Server Push는 2015년에 등장했다. 아이디어는 깔끔했다 — 클라가 요청하기 전에 서버가 미리 리소스를 푸시. 페이지가 빨라지리라.

현실은 실망스러웠다. Chrome 텔레메트리(2020~2021)에서 대다수 푸시가 — 이미 캐시에 있어서 — 낭비됐다. 캐시 동작이 미묘했고, 구현이 일관되지 않았다. Chrome은 2022년에 비활성화했다. 자료: https://developer.chrome.com/blog/removing-push. 명세에서도 제거됐다.

대안. 진짜 리소스 푸시가 필요하면 103 Early Hints + <link rel=preload>. 메시지 푸시면 SSE.


8장 · 의사결정 매트릭스

유스 케이스1순위대안비고
LLM 토큰 스트리밍SSEWS서버→클라 단방향, 재시도·CDN 친화
채팅 (1:1, 1:다)WebSocketSSE+POST양방향 저지연
멀티플레이어 게임 (실시간)WebTransportWebRTC비신뢰 데이터그램, no HoL blocking
대시보드·실시간 메트릭SSEWS단방향, 자동 재연결
협업 편집기 (CRDT)WebSocketWT양방향 + 메시지 순서
화상 통화WebRTC미디어 + 데이터 한 채널
파일 전송 P2PWebRTCWS 릴레이서버 대역폭 절감
알림 (저빈도)Long PollingSSE서버 자원·복잡도 최소
주식·코인 시세 (브로드캐스트)SSEWS단방향, fan-out 단순
IoT 명령 (양방향)WebSocketWT작은 페이로드, 양방향
폴백 마지막 한 줄Long Polling어떤 환경도 통과

이 표는 정답이 아니라 시작점이다. 모든 결정에는 트레이드오프가 있고, 팀의 운영 능력·기존 인프라·사용자 환경이 매트릭스를 흔든다.


9장 · 정직한 결정 프레임워크

다섯 가지 질문을 순서대로 던지자.

Q1. 진짜로 양방향인가, 아니면 단방향인가?

LLM 답변은 단방향이다. 프롬프트는 POST 한 번이고, 답은 토큰 스트림. 채팅 메시지도 사실 — 송신은 POST로 끝나고, 수신만 stream — 으로 보면 단방향이다.

진짜 양방향은 두 방향에서 동시에 메시지가 흐를 때다 — 협업 편집의 CRDT 업데이트, 멀티플레이어 게임의 입력/상태, IoT 양방향 명령.

단방향이면 SSE를 먼저 검토. 비용·운영·복잡도가 압도적으로 낮다.

Q2. 신뢰가 필요한가, 비신뢰면 되는가?

채팅 메시지·결제 이벤트·상태 업데이트 — 신뢰가 필수다. TCP·QUIC 스트림.

게임 좌표·VOIP·실시간 센서 — 비신뢰 OK. 한 패킷 빠져도 다음 게 따라잡으면 끝. UDP·QUIC datagram.

비신뢰 필요하면 WebTransport(또는 WebRTC). WS는 비신뢰 모드가 없다.

Q3. P2P가 정말 필요한가?

P2P는 — 정말 — 서버를 거치지 않는 경우만이다. 화상통화, 파일 전송, 진짜 P2P 게임. 그 외에는 클라이언트-서버가 압도적으로 간단하다.

P2P 한다고 결정했으면 STUN + TURN 인프라 비용·운영을 받아들이는 거다. TURN으로 폴백되면 그건 사실상 릴레이고, P2P 이득이 사라진다.

Q4. 기존 인프라는?

회사가 이미 Kafka + Pub/Sub + nginx로 SSE를 잘 돌리고 있다면 — 새 기능에 WS를 넣지 마라. 운영팀이 둘 다 알아야 한다.

WS를 잘 운영 중이라면 — SSE를 새로 도입하는 게 합리적일 수도 있다. 두 프로토콜이 다른 문제를 푼다면.

Q5. 사용자의 네트워크 환경은?

기업망·통신사망에는 — UDP 차단, WS 차단, idle 응답 차단 — 별 게 다 있다. 사용자가 다양한 환경이면 폴백이 필수다.

        WT 시도 ──실패──▶ WS 시도 ──실패──▶ SSE 시도 ──실패──▶ Long Polling

대부분의 프로덕션 실시간 스택은 이 폴백 체인을 어느 정도 갖는다.


10장 · 실전 사례 — 누가 무엇을 쓰는가

  • ChatGPT, Claude, Gemini API의 토큰 스트리밍 — SSE. 자료: OpenAI https://platform.openai.com/docs/api-reference/streaming, Anthropic https://docs.anthropic.com/en/api/messages-streaming.
  • Slack 메시지 — 한때 WebSocket. 2025년경 모바일은 HTTP 폴링 + 푸시 노티 혼합으로 이동.
  • Discord — WebSocket(메시지) + WebRTC(음성/비디오). 자료: https://discord.com/developers/docs/topics/gateway.
  • Figma 동시 편집 — WebSocket + 자체 CRDT.
  • Google Docs — Long Polling에서 출발, WebSocket으로 이행.
  • Cloudflare Stream/Workers — WebTransport 지원, 게임·라이브 비디오 워크로드를 받음.
  • Zoom — WebRTC(미디어) + 자체 시그널링.
  • GitHub Live Updates — SSE.
  • Twitch IRC (채팅) — TCP + 자체 프로토콜, 웹 클라이언트는 WebSocket 위에서 IRC를 흘림.
  • 주식 거래 플랫폼 — 거의 WebSocket(브로드캐스트 + 주문) 또는 자체 바이너리 TCP(데스크탑).

패턴이 보인다 — 단방향 스트리밍은 SSE, 양방향 채팅은 WS, 미디어는 WebRTC, 게임은 점진적으로 WT.


11장 · 운영 노트 — 진짜로 아픈 부분들

sticky session

WS·WT는 클라이언트가 같은 백엔드 인스턴스에 묶여야 한다. ALB·Envoy·HAProxy의 sticky session 설정 필요. SSE는 — 응답이 끝까지 한 인스턴스에서 흐르긴 하지만 — 재연결마다 다른 인스턴스로 가도 OK(서버가 stateless하다면).

graceful shutdown

배포할 때 WS 연결을 끊으면 사용자 1만 명이 동시에 재연결을 시도해 thundering herd. 정해진 패턴:

  1. 새 연결을 거부(헬스체크 fail).
  2. 기존 연결에 "곧 끊겠다" 메시지 전송.
  3. 클라가 jitter 두고 자발적 재연결.
  4. 30~60초 후 강제 종료.

SSE도 같은 문제. Long Polling은 — 한 요청이 짧으니 — 운이 좋다.

인증

WS의 Upgrade 핸드셰이크는 HTTP니까 쿠키·헤더·토큰 다 보낼 수 있다. 그러나 — 일부 라이브러리는 핸드셰이크 후 인증을 확인 — 이 패턴은 위험. 핸드셰이크 단계에서 인증하라.

WT는 인증서 기반 + 헤더. SSE는 — 그냥 HTTP니까 — 가장 단순.

WebRTC는 시그널링 서버 단계에서 인증. P2P 연결 자체는 DTLS-SRTP로 암호화.

비용

  • WS·SSE — 서버 컴퓨트는 연결 수에 비례. 대역폭은 메시지 수에 비례.
  • WT — 비슷하지만 QUIC CPU 오버헤드가 약간 더 크다(2026년 시점).
  • WebRTC — 서버는 시그널링·STUN·TURN. TURN 사용량이 비용 폭탄.
  • Long Polling — 연결당 CPU·메모리 + 매 요청 HTTP 헤더 오버헤드.

대규모(연결 10만 이상)에서는 — 한 연결당 메모리 — 가 결정적. Node WS는 약 3050 KB/연결, Go·Rust는 515 KB. 언어 선택이 운영비를 좌우.

모니터링

  • WS — 연결 수, 메시지 처리량, 평균 연결 수명, 재연결 빈도.
  • SSE — 활성 응답 수, 응답 수명, 데이터 인코딩 에러.
  • WT — 활성 세션, 스트림당 처리량, 데이터그램 손실률.
  • WebRTC — ICE 성공률, TURN fallback 비율, 미디어 RTT.
  • Long Polling — RPS, 보류 시간 분포.

OpenTelemetry는 — WS 일부, SSE 완전, WT는 2026년 시점 진행 중 — 지원이 다르다.


12장 · 직접 만들어보는 WT 데이터그램 서버 (Node + aiortc 없이)

브라우저 데모는 위에 있고, 서버는 Rust wtransport나 Python aioquic가 가장 흔하다. Node는 2026년 5월 현재 @fails-components/webtransport 같은 패키지가 가장 성숙.

// server.ts — 최소 WebTransport 데이터그램 에코
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
      // 에코백
      await dgWriter.write(value)
    }
  })()
}

데모용 인증서가 필요하고(브라우저가 자체서명을 받으려면 serverCertificateHashes 옵션 필요), Chrome 플래그를 켜서 테스트.


13장 · 안티패턴 10가지

  1. 모든 실시간 요구사항에 WebSocket 반사적으로 선택. 단방향이면 SSE가 더 단순·저렴.
  2. 인증을 핸드셰이크 이후에. WS upgrade 단계에서 인증하지 않으면 미인증 연결을 잠시 받게 됨.
  3. 자동 재연결을 직접 짜지 않음. 모바일 네트워크에서 끊김은 일상.
  4. 하트비트 없음. idle 프록시가 30초 후 끊을 때 모름.
  5. 메시지 sequence ID 없음. 재연결 후 중복·결손 메시지를 못 잡음.
  6. graceful shutdown 없이 재배포. thundering herd 자가 DDoS.
  7. 거대한 단일 메시지. WS의 head-of-line blocking을 자기 손으로 트리거.
  8. WebRTC를 P2P 아닌 곳에. 클라이언트-서버면 WS·WT가 더 간단.
  9. HTTP/2 Server Push를 부활시키려고 시도. 죽은 자다.
  10. 폴백 체인 없음. 사용자의 30%가 회사 프록시 뒤에 있을 수 있음.

14장 · 2026년 예측과 미래

  • WebTransport — Safari가 안정판에서 켜는 순간(2026~2027 예상) 게임·미디어 워크로드의 기본 후보가 됨.
  • HTTP/3 + SSE — 멀티플렉싱과 0-RTT 재접속이 결합돼 SSE가 더욱 강해짐.
  • MoQ (Media over QUIC) — IETF MoQ 워킹그룹이 라이브 미디어 전송 표준을 만들고 있음. 자료: https://datatracker.ietf.org/wg/moq/about/.
  • Edge runtime의 SSE 일급 지원 — Vercel, Cloudflare, Deno Deploy가 SSE를 우선시.
  • WebSocket over HTTP/3 — RFC 9220, 실제 채택은 아직 더디지만 진행 중.
  • AI 에이전트 워크플로 — 토큰 스트리밍은 SSE, 도구 호출 결과는 SSE 이벤트 타입으로 분리하는 패턴이 확산.

LLM 시대 이후의 실시간 웹은 — 한 프로토콜이 모든 걸 푸는 게 아니라 — 유스 케이스별로 최적 도구를 고르는 시대다. WS가 모든 걸 풀던 2010년대는 끝났다.


에필로그 — 프로토콜은 도구다

이 글의 한 문장 요약: "실시간"이라는 단어를 보면 먼저 양방향인지 단방향인지를 묻고, 그다음 신뢰·비신뢰를 묻고, 그다음 P2P 여부를 묻는다. 세 질문이 4~5 줄이면 답이 나온다.

LLM 토큰 스트리밍이 SSE를 고른 건 SSE가 "최고"여서가 아니다. 그 문제에 SSE가 가장 단순한 답이기 때문이다. 같은 논리로, 협업 편집은 WebSocket이 가장 단순하고, 게임 좌표는 WebTransport가 가장 자연스럽다.

도구를 사랑하지 말고, 문제를 사랑하자.

14개 항목 체크리스트

  1. 양방향이 진짜 필요한가, 사실은 단방향인가?
  2. 신뢰가 필수인가, 비신뢰면 되는가?
  3. P2P가 정말 필요한가?
  4. 사용자 네트워크 환경(기업망·모바일·해외)을 가정했는가?
  5. 폴백 체인이 있는가?
  6. 인증을 핸드셰이크 단계에서 하는가?
  7. 자동 재연결 + 백오프 + jitter가 있는가?
  8. 하트비트·idle ping이 있는가?
  9. 메시지 sequence ID와 재개 메커니즘이 있는가?
  10. graceful shutdown 절차가 있는가?
  11. sticky session·세션 친화도 설정이 라우터에 있는가?
  12. 연결당 메모리 비용을 측정했는가?
  13. 모니터링·SLO가 정의돼 있는가?
  14. (LLM이면) SSE를 먼저 검토했는가?

안티패턴 10가지 (요약)

  1. 단방향에 WebSocket.
  2. 인증 늦음.
  3. 재연결 없음.
  4. 하트비트 없음.
  5. sequence ID 없음.
  6. graceful shutdown 없음.
  7. 거대 단일 메시지.
  8. WebRTC를 P2P 아닌 곳에.
  9. HTTP/2 Server Push 부활 시도.
  10. 폴백 체인 없음.

다음 글 예고

다음 글 후보: 에지 런타임 위 SSE — Vercel/Cloudflare/Deno에서 LLM 토큰 스트리밍 패턴 비교, MoQ(Media over QUIC) 입문 — 차세대 라이브 미디어, CRDT + WebSocket — 협업 편집의 운영 노트.

"실시간 웹의 다음 10년은 한 프로토콜이 아니라 — 문제에 맞는 도구를 고르는 — 시대다. 단방향이면 SSE, 양방향이면 WebSocket, 비신뢰 다중 스트림이면 WebTransport, P2P면 WebRTC. 그리고 그래도 모자라면 Long Polling은 죽지 않았다."

— 2026 실시간 웹 심층, 끝.


참고 / References

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

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.

  • A chat line to 10,000 people worldwide within 50 ms — bidirectional, low latency.
  • LLM tokens to one user over a minute — one-way stream.
  • Game coordinates 60 times a second, where a missed packet is fine because the next one catches up — many-to-many, unreliable datagrams.
  • Video and audio for a call, plus a data side-channel — P2P media.
  • A notification to a phone every five minutes — that's push, not realtime.

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:

  • WebSocket — Bidirectional frames over a single TCP connection. Standardized 2011, most ubiquitous, also the most well-known footguns.
  • Server-Sent Events — A long-lived HTTP response that streams text. text/event-stream MIME, automatic reconnect built in. One-way, server to client.
  • WebTransport — Bidirectional over QUIC. Reliable streams plus unreliable datagrams over one connection. No head-of-line blocking. Available in Chrome stable.
  • WebRTC DataChannel — A direct connection between two peers. P2P, optionally unreliable. The hardest of the bunch to operate.
  • Long Polling — Old friend. Holds the response open and replies when an event fires. Works everywhere.

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

  • Truly bidirectional. Either side can send at any time.
  • Low overhead. One handshake, then 2 to 14 bytes of frame overhead per message.
  • Binary safe. Drop in game protocols or binary RPC unchanged.
  • Supported in essentially every browser, language, and SDK.

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

  • Simple. It's HTTP. Same headers, methods, caching rules.
  • Automatic reconnect. EventSource reconnects on drop and sends Last-Event-ID to resume.
  • Firewall friendly. Plain HTTP, traverses every corporate proxy.
  • HTTP/2 and HTTP/3 for free. Multiplexing comes along for the ride.
  • One-way = simpler servers. Maps naturally to Pub/Sub fan-out.
  • CDN- and gzip-compatible. It's a response, not an upgrade.

Weaknesses

  • Server-to-client only. Client-to-server needs a separate POST. (That's exactly the LLM API pattern.)
  • Text only. UTF-8 text — for binary you base64-encode, which is wasteful.
  • Connection cap. HTTP/1.1: 6 per origin. HTTP/2: effectively unlimited.
  • Idle timeouts. Some proxies (default nginx) terminate idle responses — you need periodic comments as keep-alives.

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

  • Chrome / Edge — Stable since Chrome 97 (2022). https://chromestatus.com/feature/4854541929873408
  • Firefox — Stable since Firefox 114 (2023). Default-on rollout still in progress.
  • Safari — Partial as of May 2026: experimental in Safari Technology Preview. Not yet enabled by default on stable.

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

Strengths

  • No head-of-line blocking. Decisive for games and multi-stream content.
  • Reliable and unreliable in one connection. Datagrams are ideal for game coordinates and side tracks.
  • Connection migration. A QUIC property — IP changes (Wi-Fi to cellular) without dropping the session.
  • TLS 1.3 mandatory. Security baked in.

Weaknesses

  • Safari incomplete. If you target every user, you need a fallback.
  • Ops tooling immature. Debugging, logging, and metrics aren't as fleshed out as WS.
  • UDP blocking. Some enterprise and carrier networks drop UDP; fallback required.
  • Certificate requirements. Real domain cert, or pin a self-signed cert via serverCertificateHashes.

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

  • Real P2P. Latency is lowest, server bandwidth is zero.
  • Unreliable mode. ordered: false, maxRetransmits: 0 for UDP-like behavior.
  • NAT traversal. STUN and TURN infrastructure connects peers behind NAT.
  • Bundled with media. Add a data channel to an existing video call cheaply.

Weaknesses

  • Hardest ops surface. Signaling server + STUN + TURN — three pieces of infra.
  • TURN is not P2P. When TURN takes over, you're really running a relay, and bandwidth bills explode.
  • Slow to connect. Hundreds of ms to seconds of ICE negotiation.
  • Subtle browser differences. Chrome, Firefox, and Safari diverge at the edges.

When to Use It

  • Video and voice with side data (chat, file transfer, game inputs).
  • True P2P games — saves server cost, minimizes latency.
  • File sharing (WebTorrent-style).
  • Metaverse and VR — frequent positional and expression data.

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

  • Works everywhere. Even HTTP/1.0. Every proxy is fine with it.
  • Standard HTTP for auth, CORS, caching, and CDN.
  • Stateless. One request, one response — the server can stay stateless.
  • Excellent fallback. When WS, SSE, and WT all fail, polling keeps the lights on.

Weaknesses

  • Header overhead. Every event pays for HTTP headers.
  • Latency jitter. There's a gap between response and the next GET.
  • Timeout tuning. Too long and proxies kill it; too short and you're really polling.

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

  • ChatGPT, Claude, Gemini token streaming — SSE. Sources: OpenAI https://platform.openai.com/docs/api-reference/streaming, Anthropic https://docs.anthropic.com/en/api/messages-streaming.
  • Slack messaging — WebSocket historically. Around 2025 mobile shifted to HTTP polling plus push notifications for battery.
  • Discord — WebSocket for messages, WebRTC for voice and video. Source: https://discord.com/developers/docs/topics/gateway.
  • Figma multi-user editing — WebSocket plus a custom CRDT.
  • Google Docs — Long Polling originally, WebSocket later.
  • Cloudflare Stream / Workers — WebTransport support, picking up gaming and live video loads.
  • Zoom — WebRTC media plus custom signaling.
  • GitHub Live Updates — SSE.
  • Twitch IRC (chat) — TCP plus a custom protocol; the web client tunnels IRC over WebSocket.
  • Stock trading platforms — Mostly WebSocket (broadcast and order entry) or a custom binary TCP for desktop.

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

  • WS and SSE — server compute scales with connection count; bandwidth scales with message volume.
  • WT — similar, with a modest QUIC CPU overhead in 2026.
  • WebRTC — server cost is signaling, STUN, and TURN. TURN usage is the bill bomb.
  • Long Polling — CPU and memory per held request plus HTTP header overhead per event.

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

  • WS — connection count, message throughput, average connection lifetime, reconnect rate.
  • SSE — active responses, response lifetime, encoding errors.
  • WT — active sessions, per-stream throughput, datagram loss rate.
  • WebRTC — ICE success rate, TURN fallback rate, media RTT.
  • Long Polling — RPS, hold-time distribution.

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

  • WebTransport — The moment Safari ships stable enable-by-default (estimated 2026 to 2027), WT becomes the default candidate for game and media loads.
  • HTTP/3 plus SSE — Multiplexing and 0-RTT reconnect make SSE even stronger.
  • MoQ (Media over QUIC) — The IETF MoQ working group is building a live media standard. Source: https://datatracker.ietf.org/wg/moq/about/.
  • First-class SSE in edge runtimes — Vercel, Cloudflare, and Deno Deploy prioritize SSE.
  • WebSocket over HTTP/3 — RFC 9220 — adoption is slow but real.
  • AI agent workflows — Token streaming over SSE, with tool-call results split out as separate SSE event types, is becoming the canonical pattern.

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