Module P-9·23 min read

WebSocket protocol vs HTTP, Socket.IO rooms and namespaces, Server-Sent Events, long polling, and scaling real-time across processes with the Redis adapter.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module P-9 — WebSockets and Real-Time Communication

What this module covers: HTTP's request-response model requires the client to ask for data. Real-time features — live chat, collaborative editing, presence indicators, live dashboards — require the server to push data the moment it changes. This module covers the WebSocket protocol upgrade, building with Socket.IO including rooms and namespaces, Server-Sent Events for one-directional streaming, long polling as a fallback, the Redis adapter and sticky sessions needed to scale real-time connections across multiple server instances, backpressure and per-socket rate limiting for misbehaving clients, re-authentication when a JWT expires mid-connection, and Redis-backed presence tracking.


HTTP vs WebSocket

HTTP is a half-duplex, stateless protocol. Every interaction is client-initiated: request → response → connection closes. The server cannot send data unless the client asks.

WebSocket is a full-duplex, stateful protocol over a single TCP connection. After the initial HTTP upgrade handshake, both sides can send frames at any time with minimal overhead (2–14 byte header per frame vs 200–800 bytes for an HTTP request).

A WebSocket connection is a phone call left off the hook — both sides can talk any time without redialing, whereas HTTP polling is hanging up and calling back every second to ask "anything new?"

text

The WebSocket handshake is a standard HTTP request with Connection: Upgrade and Upgrade: websocket headers. After the 101 response, the protocol switches and HTTP is no longer involved.


Raw WebSocket with the ws Library

bash
typescript
typescript

Backpressure: When a Slow Client Can't Keep Up

ws.send() is fire-and-forget — it queues the frame in the OS socket buffer and returns immediately, regardless of whether the client is actually reading fast enough to drain it. A client on a slow or flaky connection (a mobile device losing signal, a browser tab suspended in the background) can fall behind the server's send rate indefinitely, and nothing stops the server from continuing to call ws.send() on that same socket — unsent frames pile up in ws.bufferedAmount, growing without bound.

Production story: a blockchain indexer streamed live block events to several thousand subscribers over raw WebSocket. Most clients kept up fine. One subscriber on a flaky mobile connection didn't — ws.send() kept queuing frames for that socket every time a new block arrived, and because nothing checked bufferedAmount, the backlog was never drained or capped. That single connection's buffer grew to hundreds of megabytes before the process ran out of memory and crashed, taking down delivery for every other subscriber, including the ones with perfectly healthy connections.

The fix: check ws.bufferedAmount before sending, and close (or skip) sockets that fall too far behind instead of feeding them indefinitely:

typescript

For messages a client genuinely cannot afford to miss, an ack-based protocol (client confirms receipt, server bounds the outstanding-message window) is more correct than fire-and-forget. For a feed where the newest event supersedes the last one anyway (block updates, price ticks), disconnecting slow consumers and letting them reconnect and resync is simpler and keeps the rest of the server healthy.


Socket.IO: Rooms, Namespaces, and Events

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.