Background
Sections
IntroductionRequirements & Problem AnalysisConstraints & AssumptionsEstimation TechniquesFunctional vs Non-Functional RequirementsMoSCoW PrioritizationSystem Design FundamentalsArchitecture DiagramClass DiagramComponent DiagramData Flow Diagram (DFD)ER Diagram (Entity-Relationship Diagram)High Level Design (HLD)Low Level Design (LLD)Sequence DiagramState DiagramUse Case DiagramData StorageDocument StoresFile StorageGraph DatabasesIn-Memory DatabasesKey-Value StoresNewSQLNoSQL DatabasesObject StorageSQL Databases (RDBMS)Time-Series DatabasesWide-Column StoresDatabase ConceptsACID PropertiesCAP TheoremConsistency ModelsIndexingNormalization & DenormalizationReplicationSharding & PartitioningTransactions & Isolation LevelsScalabilityAuto-Scaling & ElasticityConsensus & Leader ElectionLoad BalancingReplication & Read ReplicasSharding & PartitioningVertical vs Horizontal ScalingAvailability & ReliabilityBackup & Data DurabilityCircuit BreakerData ConsistencyDisaster RecoveryFault Tolerance & FailoverGraceful DegradationHigh AvailabilityNetworkingCDNDNSFirewalls & VPNHTTP & HTTPSLoad Balancer & Reverse ProxyTCP/IP & UDPWebSocketsCachingCache InvalidationCache Read/Write PatternsCaching LayersEviction PoliciesRedis vs MemcachedMessaging & CommunicationDead-Letter QueueIdempotencyKafka vs RabbitMQ vs SQSMessage QueuesPub/SubCompute & ServicesAPI GatewayContainers & OrchestrationMonolith vs MicroservicesServerlessService DiscoveryService MeshWeb Server & App ServerAPI DesignAPI Versioning & IdempotencyAuthentication & AuthorizationGraphQLgRPCPaginationRate Limiting & ThrottlingRESTSecurityAuthentication & AuthorizationData PrivacyEncryptionInput Validation & InjectionOAuth2 & JWTSecrets ManagementXSS & CSRFStorage & File SystemsBackup & RetentionBlock vs File vs Object StorageData Lakes & WarehousesDistributed File SystemsEphemeral StorageObservability & MonitoringDistributed TracingHealth ChecksLoggingMetricsSLI, SLO, SLADesign PatternsBulkhead PatternCircuit Breaker PatternCreational PatternsRate Limiter PatternRetry PatternStructural & Behavioral Patterns

WebSockets

7 min read

In a Nutshell

WebSockets provide a persistent, two-way (full-duplex) communication channel between a client and a server over a single long-lived connection. Ordinary HTTP is request-response — the client asks, the server answers, and the connection is effectively one-directional and short-lived. That's a poor fit for anything real-time: chat, live sports scores, collaborative editing, trading dashboards, multiplayer games. WebSockets solve this by upgrading an HTTP connection into a durable pipe where either side can send a message at any time, with almost no per-message overhead. When you need the server to push data to clients instantly, WebSockets are the go-to.

2D minimalistic diagram contrasting two panels: left labeled "HTTP" shows a client repeatedly asking and a server answering in separate short-lived request-response arrows; right labeled "WebSocket" shows one persistent bidirectional pipe where both client and server push messages freely at any time

How It Actually Works

From HTTP to a Persistent Socket

A WebSocket connection begins as a normal HTTP request with an Upgrade header, then switches protocols on the same TCP connection:

Client → Server (HTTP upgrade request):
  GET /ws HTTP/1.1
  Host: example.com
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Server → Client (agrees to switch):
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

── From here the TCP connection carries WebSocket frames both ways ──
── ws:// (plaintext) or wss:// (over TLS, the standard) ──

After the 101 handshake, there's no more HTTP overhead — just lightweight message frames flowing in both directions until either side closes.

The Real-Time Communication Options Compared

WebSockets aren't the only way to get server-to-client updates. Know the trade-offs:

Technique Direction Connection Best For
Short polling Client pulls repeatedly New request each time Simple, infrequent updates
Long polling Client pulls, server holds until data Held then re-opened Near-real-time without WebSockets
Server-Sent Events (SSE) Server → client only One long-lived HTTP Server push (feeds, notifications)
WebSockets Full-duplex both ways One persistent socket Chat, gaming, collaboration
WebRTC Peer-to-peer Direct (UDP) Low-latency audio/video/data
Polling:      client: "anything new?" ... "anything new?" ... (wasteful)
Long poll:    client: "anything new?" → server waits → replies when it is
SSE:          server ───stream of events──▶ client   (one direction)
WebSocket:    client ◀════full-duplex════▶ server    (both directions)

Decision heuristic: need only server→client push? SSE is simpler and auto-reconnects over plain HTTP. Need genuine two-way, low-latency messaging? WebSockets. Need media/peer-to-peer? WebRTC.

The Hard Part: Scaling Stateful Connections

WebSockets are stateful — each connection lives on a specific server for its whole lifetime. This breaks the easy statelessness that makes HTTP horizontally scalable, and creates real challenges:

Challenge Why It's Hard Solution
Load balancing Connections are long-lived and sticky L4 LB or sticky sessions; connection-aware routing
Broadcasting A message for user X may need a server that doesn't hold X's socket A pub/sub backplane (Redis, Kafka) fans messages across servers
Connection limits Each socket consumes memory + a file descriptor Tune OS limits; scale out; use efficient async servers
Reconnection Networks drop; mobile clients sleep Auto-reconnect + resume with last-seen message ID
Scaling out Adding a server doesn't rebalance existing connections Graceful connection draining on deploy

The Pub/Sub Backplane Pattern

The key architectural pattern for scaling WebSockets across many servers: servers don't talk to each other directly; they publish and subscribe through a shared message bus.

User A's socket → Server 1        Server 2 → User B's socket
                    │                 ▲
                    │  publish        │  subscribe
                    ▼                 │
              ┌───────────────────────────┐
              │  Redis Pub/Sub  (backplane) │
              └───────────────────────────┘
A sends a message → Server 1 publishes to Redis channel →
Server 2 (subscribed) receives it → delivers to User B.
Any server can reach any user regardless of which server holds the socket.

2D minimalistic diagram showing WebSocket scaling with a pub/sub backplane: two application servers each holding several client WebSocket connections, both connected to a central Redis pub/sub bus in the middle; a message from a client on server 1 flows up to Redis and back down to a recipient client on server 2, illustrating cross-server message delivery

Seeing It in Action

Scenario: A chat application's message flow.

// --- Client ---
const ws = new WebSocket("wss://chat.example.com/ws");

ws.onopen  = () => ws.send(JSON.stringify({ type: "join", room: "general" }));
ws.onmessage = (e) => renderMessage(JSON.parse(e.data));   // server PUSHES
ws.onclose = () => scheduleReconnect();                    // resilience

function sendChat(text) {
  ws.send(JSON.stringify({ type: "msg", room: "general", text }));
}

// --- Server (pseudocode) ---
onMessage(socket, msg) {
  if (msg.type === "msg") {
    // Publish to the room's channel — every server subscribed to it
    // will deliver to its locally-connected members of that room.
    redis.publish(`room:${msg.room}`, serialize(msg));
    persist(msg);                       // store for history/offline users
  }
}

onRedisMessage(`room:general`, msg) {
  for (socket of localSocketsInRoom("general"))
    socket.send(msg);                   // fan out to this server's clients
}

Why the backplane is essential: two users in the same chat room may be connected to different servers. Without the Redis pub/sub layer, a message arriving at server 1 could never reach a user on server 2. The backplane decouples "which server holds the socket" from "who should receive the message," which is exactly what lets a stateful, connection-oriented system scale horizontally. Add persistence so users who reconnect can catch up on missed messages.

Interview Questions

  1. Q: How does a WebSocket connection get established, and how does it differ from HTTP after that? Hint: It starts as an HTTP GET with Upgrade: websocket/Connection: Upgrade headers; the server replies 101 Switching Protocols, and the same TCP connection is repurposed to carry WebSocket frames. After the handshake there's no HTTP request-response overhead — either side can send lightweight message frames at any time (full-duplex) until the connection closes. Use wss:// (TLS) in production.

  2. Q: When would you use Server-Sent Events instead of WebSockets? Hint: When you only need server→client push (feeds, notifications, live scores). SSE is simpler, runs over plain HTTP, auto-reconnects with Last-Event-ID, and works through most proxies. Choose WebSockets when you need genuine bidirectional, low-latency messaging (chat, gaming, collaborative editing) where the client also frequently pushes to the server.

  3. Q: Why are WebSockets harder to scale horizontally than HTTP services? Hint: They're stateful and long-lived — each connection is pinned to one server for its lifetime, breaking the "any server handles any request" model. Load balancing must keep connections sticky, adding a server doesn't rebalance existing connections, each socket consumes memory/file descriptors, and delivering a message to a user may require a server that doesn't hold their socket.

  4. Q: How do you deliver a message to a user connected to a different server than the sender? Hint: Use a pub/sub backplane (Redis, Kafka, NATS). Servers publish messages to channels and subscribe to the ones relevant to their connected clients; the bus fans messages across all servers. This decouples "which server holds the socket" from "who should receive the message," enabling cross-server delivery and horizontal scaling.

  5. Q: How do you make a WebSocket-based app resilient to dropped connections? Hint: Client-side auto-reconnect with exponential backoff; resume state using a last-seen message ID / sequence number so the client can request what it missed; server-side persistence of messages for offline/reconnecting users; heartbeats/pings to detect dead connections; and graceful connection draining during deploys so clients reconnect in a controlled way.

References

Dive Deeper