Library
00/07 · ~38 min
GUIDEDECK · for apps that can't wait for the next request

WebSockets &
Realtime,
push over poll.

A 38-minute working session on pushing data the moment it changes — the WebSocket handshake and frames, the connection lifecycle you must manage (heartbeats, reconnect), the pub/sub and presence patterns realtime apps are built from, how to scale across many servers with a backplane, and when a lighter transport (SSE, long-polling, WebRTC) is the better call — with the real libraries and managed services you'll actually use.

~38 MINBACKEND / FULL-STACKPROTOCOL
SCROLL
01 · Why realtime, and why polling hurts 4 min

The web was built to pull;
realtime is about push.

Plain HTTP is request/response: the client asks, the server answers, the connection closes. That's perfect for loading a page — and wrong for anything that changes on its own. Chat, live dashboards, multiplayer cursors, order tracking, notifications: the server knows first, so the server should speak first.

Realtime here means server-initiated, low-latency updates: the server pushes data the instant it changes instead of the client asking again and again. The bar is "feels live" — typically under a few hundred milliseconds end to end.

Polling: faking realtime over request/response

Short polling — ask again and again
// re-ask on a timer, hope something changed setInterval(async () => { const r = await fetch('/api/messages') render(await r.json()) }, 3000) // ✕ up to 3s late · ✕ mostly empty replies // ✕ N clients = N× constant request load
WebSocket — one connection, server pushes
// open once; receive the moment data changes const ws = new WebSocket('wss://api.app/feed') ws.onmessage = (e) => render(JSON.parse(e.data)) // ✓ instant · ✓ no wasted round-trips // ✓ full-duplex: client can send too
SHORT POLLING data 4 wasted trips, then late by up to one interval PUSH (WebSocket) open data ▸ pushed

Polling's average latency is half the interval, and most replies are empty. A push arrives once, exactly when there's something to send.

What polling actually costs

  • Latency — on average half your interval; shorten it and the waste grows.
  • Wasted work — most responses are 304/empty, yet still cost a round-trip and a DB hit.
  • Server load — scales with clients × frequency, even when nothing changes.
  • Battery & data — constant requests drain mobile devices.

Like  phoning the kitchen every minute to ask if dinner's ready — versus the cook calling you the moment it is.

02 · The handshake and the wire format 6 min

A WebSocket starts life as
an HTTP request — then upgrades.

You don't open a WebSocket out of thin air. It begins as an ordinary HTTP GET carrying an Upgrade header; if the server agrees, the same TCP connection stops speaking HTTP and starts speaking the WebSocket protocol — full-duplex, both sides free to send at any time.

The WebSocket protocol (RFC 6455) gives you a single long-lived, two-way connection over one TCP socket. URLs use ws:// or, over TLS, wss:// — always prefer wss in production (encrypted, and far more likely to survive proxies).
// client asks to switch protocols GET /feed HTTP/1.1 Host: api.app Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 // server agrees — and the socket is now open HTTP/1.1 101 Switching Protocols Upgrade: websocket Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After 101: frames, not requests

  • Data travels in small frames, each tagged with an opcode: text, binary, ping, pong, or close.
  • Frames are tiny — a 2-byte header for short messages, no URL, no cookies, no headers re-sent. That's why they're cheap.
  • Browser→server frames are masked (a security rule); you never deal with this by hand.
  • A big message can be fragmented across frames and reassembled — you still get one onmessage.
FIN + opcode mask + length mask key payload a few header bytes your data

A frame is mostly payload: a couple of header bytes, then your bytes. No per-message HTTP overhead.

03 · The connection lifecycle 6 min

A socket has a lifecycle
and it will drop.

A long-lived connection is a liability as much as a feature: laptops sleep, Wi-Fi flickers, phones change networks, and proxies quietly cut idle connections. Realtime code is mostly about surviving that — detecting a dead socket fast and getting back online cleanly.

The browser gives you four events — open, message, close, error. The two that decide whether your app feels reliable, you add yourself: a heartbeat (to notice a dead link) and reconnect with backoff (to recover without stampeding the server).
connecting
open
send / receive
closed
drop or error
reconnecting
backoff, then retry

The happy path is one hop; the real work is the loop — closed → reconnecting → (after a backoff delay) → connecting.

Heartbeat — notice a dead link

// ping on a timer; expect a pong back setInterval(() => ws.send('{"t":"ping"}'), 15000) // no pong within a few seconds? // the socket is half-open — treat it as dead // and trigger a reconnect.

Reconnect — back off, don't stampede

let wait = 1000 ws.onclose = () => { setTimeout(connect, wait) // exponential backoff, capped + jittered wait = Math.min(wait * 2, 30000) }
Why both matter: without a heartbeat a "half-open" socket looks alive but silently drops messages; without backoff, every client reconnecting at once after an outage becomes a self-inflicted DDoS the moment your server comes back.
04 · The patterns realtime is built from 6 min

One event, many listeners.

Very few realtime apps are one-to-one. The dominant shape is publish/subscribe: clients subscribe to a named channel, and when something happens the server broadcasts it to everyone subscribed — without the publisher knowing who's listening.

Pub/sub — a publisher sends to a channel (a.k.a. room / topic); every subscriber of that channel receives it. Presence is the bookkeeping on top: who is currently in a channel, plus join/leave (and "typing…") events.
publisher
posts a message
server
channel "room:42"
subscriber A
subscriber B
subscriber C

Publish once to a channel; the server fans it out to every current subscriber. The publisher never names the recipients.

Channels / rooms

Address a subset

Group sockets under a name (room:42, user:7) so a broadcast reaches exactly the right clients — not everyone connected.

Presence

Who's here, right now

Track membership of a channel and emit join/leave. Powers "3 people online", avatars, and live "typing…" indicators.

Backpressure

Slow clients exist

A consumer that can't keep up forces a choice: buffer (memory risk), drop (lossy), or disconnect. Decide deliberately per channel.

// server (Socket.IO-style rooms) socket.join('room:42') io.to('room:42').emit('message', payload) // only sockets that joined room:42 receive it
05 · Scaling realtime across servers 6 min

Many servers, one conversation.

A single process can hold tens of thousands of sockets, but eventually you run more than one. The moment you do, naive broadcast breaks: two users in the same chat may be connected to different servers, and a message published on one never reaches the other.

The core problem of scaling realtime: a message published on server A must reach a subscriber connected to server B. The fix is a backplane — a shared pub/sub bus (Redis, NATS, Kafka) that relays every message between your nodes so they act like one.
load balancer
sticky by client
server A
server B
Redis pub/sub
backplane

A message arriving on server A is published to Redis, which delivers it to server B — so B's clients see it too. Every node both publishes and subscribes.

1
Sticky sessions
A socket is stateful — keep each client on its server.
+
  • A WebSocket lives on one process; the load balancer must pin a client to the server it connected to (by IP hash or a cookie).
  • Without stickiness, a reconnect can land on a different node mid-session and lose in-memory state.
2
The backplane
A shared bus so any node can reach any client.
+
  • Redis pub/sub — simplest and most common; great for fan-out, no persistence.
  • NATS — lightweight, very fast, built for this.
  • Kafka — when you also need durability/replay, at higher operational cost.
3
Connection limits
Each socket costs a file descriptor + memory.
+
  • Tune OS limits (file descriptors) and budget memory per connection.
  • Idle connections still cost — heartbeats and sensible timeouts keep the count honest.
4
Presence at scale
Membership has to be shared, not per-node.
+
  • "Who's online" can't live in one process's memory once you have many — keep it in a shared store (Redis).
  • This is a big reason teams reach for a managed realtime service instead of building it.

Most of this — stickiness, the backplane, presence, connection limits — is exactly what a managed realtime service handles for you. Build it yourself when you need control; buy it when you'd rather ship.

06 · When NOT to use a WebSocket 5 min

WebSocket isn't always the answer.

A full-duplex socket is powerful but heavy to run. Three other transports cover a lot of realtime needs with less operational weight — and one of them (SSE) is the right default for the most common case of all: server→client feeds.

Match the transport to the direction and shape of your data, not to the hype. Two-way and chatty → WebSocket. One-way server→client → SSE. Media or peer-to-peer → WebRTC. Stuck behind a hostile proxy → fall back to long-polling.

Server-Sent Events — one-way, over plain HTTP

const es = new EventSource('/api/stream') es.onmessage = (e) => render(e.data) // auto-reconnect is built in. text only.
Pro
Dead simple, rides ordinary HTTP, and reconnects automatically with an event id.
Con
Server→client only, text only; old HTTP/1 caps simultaneous connections per domain.

Use when: notifications, live scores, progress, log/AI token streams — anything the client only needs to receive.

Long-polling — the universal fallback

The client makes a request the server holds open until there's data (or a timeout), then immediately re-requests. It mimics push using only normal HTTP.

Pro
Works literally everywhere — no special protocol, sails through old proxies and firewalls.
Con
Higher latency and overhead from re-establishing each request; a compatibility fallback, not a goal.

Use when: you need a floor of support; libraries like Socket.IO use it automatically when WebSockets are blocked.

WebRTC — peer-to-peer media & data

Direct browser-to-browser channels for audio, video, and low-latency data — the server mostly just helps the peers find each other (signaling).

Pro
Lowest latency and P2P (traffic skips your server); purpose-built for media.
Con
Complex — needs signaling plus STUN/TURN servers to punch through NATs/firewalls.

Use when: video/voice calls, screen share, or P2P games where every millisecond counts.

realtime need
SSE
server → client
WebSocket
two-way, chatty
WebRTC
media / P2P
long-polling
fallback

Start from the need: most "live" features are one-way and SSE is enough; reach for a WebSocket when the client must talk back too.

07 · Tooling, a build-vs-buy call & recap 5 min

The tools you'll actually reach for.

You rarely hand-roll RFC 6455. The real choice is build-vs-buy: a library you run yourself, or a managed service that owns the hard parts (scaling, presence, global delivery). Here are the leading options, each with one honest upside and downside.

A library/server you host gives you control and no per-message bill, but you own scaling, reconnection and presence. A managed service takes those over for a usage fee. Pick by how core realtime is — and how much ops you want to carry.

Libraries & self-hosted servers

ws (Node)

The minimal WebSocket library

A tiny, standards-pure WebSocket implementation for Node. You get raw frames and nothing else.

  • Pro — small, fast, no magic; perfect when you want exactly WebSockets.
  • Con — rooms, reconnect, fallbacks and scaling are all yours to build.
Socket.IO

Batteries-included realtime

A higher-level layer with rooms, auto-reconnect, long-poll fallback and a Redis adapter for scaling out.

  • Pro — huge ecosystem; rooms, reconnection and fallbacks out of the box.
  • Con — its own wire protocol (not raw WS), and heavier than plain frames.
Centrifugo / Soketi

Self-hostable realtime servers

Standalone, language-agnostic pub/sub servers (Soketi is Pusher-protocol-compatible) you run beside your app.

  • Pro — scales and does presence out of the box; your app just publishes.
  • Con — another service to deploy, monitor and operate.

Managed realtime

Ably

Managed pub/sub, global edge

Hosted channels with presence, message history and delivery guarantees across a global network.

  • Pro — strong reliability/SLAs, presence and history built in.
  • Con — usage cost grows with scale; a third-party dependency.
Pusher

Simple hosted channels

One of the original hosted pub/sub services — quick to wire up for broadcasts and presence.

  • Pro — fast to integrate, mature SDKs, generous for small apps.
  • Con — connection/message limits and cost climb as you grow.
Supabase Realtime

Realtime tied to your database

Open-source service that streams Postgres row changes plus broadcast and presence channels.

  • Pro — database-integrated (listen to data changes directly); open-source.
  • Con — best inside the Supabase/Postgres model; less of a standalone bus.

How to choose: ws when you want pure WebSockets and will own scaling; Socket.IO for a full toolkit in one app; Centrifugo/Soketi to self-host a scalable bus; Ably/Pusher/Supabase when you'd rather pay to make scaling, presence and global delivery someone else's problem.

1Push beats poll — when it earns it. Use realtime for frequent, latency-sensitive updates; polling is fine for the occasional check.
2The connection will drop. Heartbeats to detect dead sockets and reconnect-with-backoff aren't optional.
3Realtime is pub/sub. Model channels/rooms and presence early — almost everything is one-to-many.
4Scaling needs a backplane. The moment you run two servers, add Redis/NATS so any node can reach any client.
5Pick the lightest transport. SSE for one-way feeds, WebRTC for media/P2P, long-poll as a fallback — WebSocket when you truly need two-way.
Knowledge check

Did it stick?

Five quick questions on realtime transports, the WebSocket handshake, lifecycle and scaling — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library