Server

WebSocket vs Long Polling vs SSE

By Prabath Thalangama· October 24, 2026· 4 min read
#websocket#http#real-time

Introduction

"Push data to the browser" has three common answers: long polling, Server-Sent Events (SSE), and WebSocket. They trade off simplicity, efficiency, direction, and how well they survive corporate proxies.

Long polling

The client makes an HTTP request; the server holds it open until it has data (or a timeout), responds, and the client immediately makes another request.

GET /events           ---->  (server holds... 25s... event arrives)
                      <----  200 {"type":"message",...}
GET /events           ---->  (held again)
  • Pros: plain HTTP, works through any proxy/firewall, trivial to load balance, degrades gracefully.
  • Cons: a full request/response (headers, ~hundreds of bytes) per message; a brief window between responses where events must be queued server-side; higher latency under load.

Still the right choice when you must traverse unknown middleboxes or support ancient clients.

Server-Sent Events (SSE)

One long-lived HTTP response with Content-Type: text/event-stream; the server writes events into it over time.

GET /stream
Accept: text/event-stream
          ---->
          <----  200  Content-Type: text/event-stream
                 data: {"price": 42}\n\n
                 data: {"price": 43}\n\n
                 ...
  • Pros: dead simple, native EventSource API with automatic reconnect and Last-Event-ID resume, works over plain HTTP/1.1 and HTTP/2, one TCP connection.
  • Cons: server → client only; the client still needs a normal POST for anything it sends. Text only (base64 your binary). Over HTTP/1.1, each SSE stream eats one of the browser's ~6 connections per origin — HTTP/2 fixes that with multiplexing.

Great fit for dashboards, notifications, live feeds, LLM token streaming.

WebSocket

Starts as an HTTP request with an Upgrade header; after a 101 the same TCP connection becomes a bidirectional message pipe with its own tiny framing.

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
          ---->
          <----  HTTP/1.1 101 Switching Protocols
                 Upgrade: websocket
                 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
   <==== frames both directions, ~2–14 bytes overhead each ====>
  • Pros: full duplex, minimal per-message overhead, binary or text, low latency.
  • Cons: not HTTP after the upgrade — proxies, WAFs, and L7 load balancers must explicitly support it. No built-in reconnect/resume (you build it). Sticky sessions or a shared pub/sub backend (Redis) needed to scale horizontally.

The framing (WebSocket)

Each frame: a 2-byte header (FIN bit, opcode — text/binary/close/ping/pong — and payload length), an optional extended length, a 4-byte mask (client→server frames are XOR-masked to defeat proxy cache poisoning), then the payload. Ping/ pong frames are the keepalive mechanism — send them or an idle connection gets reaped.

Proxy and timeout gotchas

# Nginx must be told to pass the Upgrade headers through
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;   # map $http_upgrade -> "upgrade"/""
    proxy_read_timeout 3600s;      # default 60s kills idle WS/SSE connections
    proxy_buffering off;           # critical for SSE — otherwise events are buffered
}
  • SSE events arrive in bursts, not live — a proxy is buffering the response. proxy_buffering off (Nginx), X-Accel-Buffering: no header, disable gzip on the stream.
  • WebSocket connects then drops after 60s — proxy_read_timeout (or an ELB idle timeout, or Cloudflare's 100s). Raise it and send app-level pings.
  • 101 never happens, get a 200 or 400 — the proxy isn't forwarding Upgrade/Connection, or a WAF is stripping them.
  • Long polling hammered the server — no jitter/backoff on reconnect after an error; a thundering herd of clients all re-polling in lockstep.

Choosing

Need Use
Server → client updates, simple SSE
Bidirectional, chat, games, collab editing WebSocket
Must work through unknown/hostile proxies Long polling
LLM token streaming SSE (or WS if you're already on it)

Verification and troubleshooting

# Test the WS handshake by hand
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" -H "Sec-WebSocket-Version: 13" \
  http://example.com/ws          # expect: HTTP/1.1 101

# Test SSE
curl -N -H "Accept: text/event-stream" http://example.com/stream   # events should stream live

websocat ws://example.com/ws     # interactive WS client
  • Browser DevTools → Network → WS tab shows frames in/out for a WebSocket; the EventStream tab shows SSE events.
  • Scaling WebSocket — a message published on instance A must reach a client connected to instance B. Put a Redis pub/sub (or NATS, or a managed service) behind all instances; don't rely on sticky sessions alone.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.