WebSockets for Real-Time Features

Reviewed & published by Brayan K

A WebSocket is a persistent, two-way connection between a browser and a server over a single TCP link, letting both sides send messages to each other instantly — ideal for chat, live updates, and other real-time features.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

WebSocket examples require a server to connect to. For the best experience: Download Node.js to run a local WebSocket server, use your browser's Developer Console (Press F12) to test the WebSocket API, or try online WebSocket testing services.

Master real-time communication: persistent connections, messaging patterns, presence, rooms, and scalable architecture.

What You'll Learn

What WebSockets Actually Are

HTTP is request → response only. WebSockets allow persistent, two-way communication: the server can push events anytime, and the client can send events anytime — no need for repeated HTTP requests.

HTTP (Traditional)

WebSockets

WebSockets Power:

Chat systems • Live notifications • Real-time games • Stock tickers • Collaborative editing • Multiplayer interactions • Typing indicators • Presence systems • Live dashboards

Basic WebSocket Connection

The browser's WebSocket API is simple but powerful. Always use wss:// for secure connections.

// Basic WebSocket Connection
const socket = new WebSocket("wss://echo.websocket.org");

// Connection opened
socket.addEventListener("open", (event) => {
  console.log("✅ Connected to WebSocket server!");
  
  // Send a message
  socket.send("Hello Server!");
});

// Listen for messages
socket.addEventListener("message", (event) => {
  console.log("📨 Message from server:", event.data);
});

// Connection closed
socket.addEventListener("close", (event) => {
  console.log("❌ Disconnected from server");
  console.log("Code:", event.code, "Reason:", event.reason);
});

// Handle errors
socket.addEventListener("error", (error) => {
  console.error("⚠️ WebSocket error:", error);
});

// Check connection state
console.log("Ready state:", socket.readyState);
// 0 = CONNECTING
// 1 = OPEN
// 2 = CLOSING
// 3 = CLOSED

// Simulated for demo
console.log("\n💡 WebSocket states:");
console.log("CONNECTING:", WebSocket.CONNECTING);
console.log("OPEN:", WebSocket.OPEN);
console.log("CLOSING:", WebSocket.CLOSING);
console.log("CLOSED:", WebSocket.CLOSED);

Real-Time Messaging with JSON

WebSockets only send text or binary. Best practice is to use structured JSON messages with type fields to handle different event types.

// Real-Time Messaging with JSON

// Message types for a chat app
const MessageTypes = {
  CHAT: 'chat',
  TYPING: 'typing',
  PRESENCE: 'presence',
  JOIN_ROOM: 'join_room',
  LEAVE_ROOM: 'leave_room',
  ACK: 'ack',
  ERROR: 'error'
};

// Create a structured message
function createMessage(type, payload) {
  return JSON.stringify({
    id: crypto.randomUUID(),
    type,
    payload,
    timestamp: Date.now()
  });
}

// Send chat message
const chatMessage = createMessage(MessageTypes.CHAT, {
  text: "Hello everyone!",
  room: "general",
  user: "Boopie"
});

console.log("Chat message:", chatMessage);

// Send typing indicator
const typingMessage = createMessage(MessageTypes.TYPING, {
  room: "general",
  user: "Boopie",
  isTyping: true
});

console.log("\nTyping message:", typingMessage);

// Parse incoming messages safely
function parseMessage(data) {
  try {
    const msg = JSON.parse(data);
    
    // Validate required fields
    if (!msg.type || !msg.payload) {
      throw new Error("Invalid message structure");
    }
    
    return msg;
  } catch (error) {
    console.error("Failed to parse message:", error.message);
    return null;
  }
}

// Handle different message types
function handleMessage(data) {
  const msg = parseMessage(data);
  if (!msg) return;
  
  switch (msg.type) {
    case MessageTypes.CHAT:
      console.log(`[${msg.payload.user}]: ${msg.payload.text}`);
      break;
    case MessageTypes.TYPING:
      console.log(`${msg.payload.user} is typing...`);
      break;
    case MessageTypes.PRESENCE:
      console.log(`${msg.payload.user} is ${msg.payload.status}`);
      break;
    default:
      console.log("Unknown message type:", msg.type);
  }
}

// Test the handler
handleMessage(chatMessage);
handleMessage(typingMessage);

Robust Reconnection Logic

WebSocket connections will always drop eventually (network issues, server restarts, etc.). Production apps must auto-recover with exponential backoff.

// Robust Reconnection Logic with Exponential Backoff

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.maxRetries = options.maxRetries || 10;
    this.baseDelay = options.baseDelay || 1000;
    this.maxDelay = options.maxDelay || 30000;
    
    this.retries = 0;
    this.socket = null;
    this.listeners = new Map();
    this.messageQueue = [];
    
    this.connect();
  }
  
  connect() {
    console.log(`Connecting to ${this.url}...`);
    
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log("✅ Connected!");
      this.retries = 0;
      
      // Send queued messages
      while (this.messageQueue.length > 0) {
        const msg = this.messageQueue.shift();
        this.socket.send(msg);
      }
      
      this.emit("open");
    };
    
    this.socket.onclose = (event) => {
      console.log("❌ Connection closed:", event.code);
      this.emit("close", event);
      
      if (this.retries < this.maxRetries) {
        this.scheduleReconnect();
      } else {
        console.error("Max retries reached. Giving up.");
        this.emit("maxRetries");
      }
    };
    
    this.socket.onerror = (error) => {
      console.error("WebSocket error:", error);
      this.emit("error", error);
    };
    
    this.socket.onmessage = (event) => {
      this.emit("message", event);
    };
  }
  
  scheduleReconnect() {
    this.retries++;
    
    // Exponential backoff with jitter
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.retries - 1) + Math.random() * 1000,
      this.maxDelay
    );
    
    console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.retries})`);
    
    setTimeout(() => this.connect(), delay);
  }
  
  send(data) {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      // Queue message for when connection is restored
      this.messageQueue.push(data);
      console.log("Message queued (not connected)");
    }
  }
  
  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }
  
  emit(event, data) {
    const callbacks = this.listeners.get(event) || [];
    callbacks.forEach(cb => cb(data));
  }
  
  close() {
    this.maxRetries = 0; // Prevent reconnection
    this.socket?.close();
  }
}

// Usage example
console.log("Creating reconnecting WebSocket...");
const ws = new ReconnectingWebSocket("wss://example.com/chat");

ws.on("open", () => console.log("Ready to send messages!"));
ws.on("message", (event) => console.log("Received:", event.data));
ws.on("maxRetries", () => console.log("Connection failed permanently"));

// Simulate sending
ws.send(JSON.stringify({ type: "ping" }));
console.log("\n💡 Messages are queued if not connected!");

Worked Example — Run a WebSocket Client With No Server

Everything above is real WebSocket code, and none of it can run on this page, because a WebSocket needs a live server at the other end. That is a genuine problem when you are learning: you cannot see the queueing, the routing or the reconnect logic actually happen.

So use the technique that professional teams use for exactly this reason. Write your client against a fake socket — a plain object with the same shape as a real one: the same readyState numbers, the same send(), the same onopen/onmessage/onclose. Your client cannot tell the difference, and you get to make the "server" accept, send and drop the connection whenever you like. This is how you test the awkward cases — a dropped connection mid-send — that are nearly impossible to reproduce against a real server on demand.

The program below runs a full connection lifecycle: a message written before the handshake finished, the flush when it opens, three incoming messages including one nobody handles, then the network dying. Read the output beside the code.

// WORKED EXAMPLE - a WebSocket client you can actually run.
// A real socket needs a server, which this editor has not got. So here is the
// trick professionals use to build and test socket code offline: swap the
// socket for a FAKE object with the same shape. The client cannot tell the
// difference, and you get to fire any event you like, on demand.

// ---------- The stand-in for a real WebSocket ----------
class FakeSocket {
  constructor(url) {
    this.url = url;
    this.readyState = 0;    // 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED - the real numbers
    this.sent = [];         // everything the client handed to send()
    this.onopen = null;
    this.onmessage = null;
    this.onclose = null;
  }
  send(data) {
    // A real WebSocket throws in exactly this case if you send too early.
    if (this.readyState !== 1) throw new Error("InvalidStateError: socket is not open");
    this.sent.push(data);
  }
  // ---- test-only handles. On a real socket the NETWORK causes these. ----
  serverAccepts() { this.readyState = 1; if (this.onopen) this.onopen(); }
  serverSends(obj) { if (this.onmessage) this.onmessage({ data: JSON.stringify(obj) }); }
  serverDrops(code) { this.readyState = 3; if (this.onclose) this.onclose({ code: code, reason: "" }); }
}

// Exponential backoff with a ceiling: 1s, 2s, 4s, 8s... never past 30s.
// It is pure arithmetic, which is why it is the easiest part to get right.
function backoff(attempt) {
  return Math.min(1000 * Math.pow(2, attempt - 1), 30000);
}

// ---------- The client: the part you actually write ----------
class ChatClient {
  constructor(url) {
    this.outbox = [];     // messages written while the socket was not open
    this.handlers = {};   // message type -> what to do about it
    this.attempt = 0;     // how many reconnects in a row
    this.socket = new FakeSocket(url);
    this.wire();
  }

  wire() {
    this.socket.onopen = () => {
      console.log("open: flushing " + this.outbox.length + " queued message(s)");
      this.attempt = 0;                                  // a good connection resets the backoff
      while (this.outbox.length > 0) this.socket.send(this.outbox.shift());
    };

    this.socket.onmessage = (event) => {
      // event.data is always TEXT. Never trust it to be valid JSON.
      let msg;
      try {
        msg = JSON.parse(event.data);
      } catch {
        console.log("ignored a message that was not JSON");
        return;
      }
      const handler = this.handlers[msg.type];           // route on the type field
      if (!handler) {
        console.log("no handler for type '" + msg.type + "'");
        return;
      }
      handler(msg.payload);
    };

    this.socket.onclose = (event) => {
      this.attempt++;
      console.log("closed with code " + event.code + " - would retry in " + backoff(this.attempt) + "ms");
    };
  }

  on(type, fn) { this.handlers[type] = fn; }

  send(type, payload) {
    const text = JSON.stringify({ type: type, payload: payload });
    if (this.socket.readyState !== 1) {
      this.outbox.push(text);                            // do not throw at the user
      console.log("queued (socket not open): " + type);
      return;
    }
    this.socket.send(text);
    console.log("sent: " + type);
  }
}

// ---------- Drive it through a whole connection lifecycle ----------
const client = new ChatClient("wss://example.invalid/chat");
client.on("chat", p => console.log("chat from " + p.user + ": " + p.text));
client.on("presence", p => console.log(p.user + " is now " + p.status));

client.send("chat", { user: "you", text: "anyone there?" });   // still connecting
client.socket.serverAccepts();                                  // handshake completes
client.send("chat", { user: "you", text: "hello!" });

client.socket.serverSends({ type: "chat", payload: { user: "ada", text: "hi" } });
client.socket.serverSends({ type: "presence", payload: { user: "ada", status: "online" } });
client.socket.serverSends({ type: "typing", payload: {} });     // nothing registered for this

client.socket.serverDrops(1006);                                // 1006 = lost, no close frame
client.send("chat", { user: "you", text: "still here?" });      // queued again, not lost

console.log("backoff schedule (ms): " + [1, 2, 3, 4, 5, 6].map(backoff).join(", "));
console.log("server actually received " + client.socket.sent.length + " message(s)");

// ✅ Expected output:
// queued (socket not open): chat
// open: flushing 1 queued message(s)
// sent: chat
// chat from ada: hi
// ada is now online
// no handler for type 'typing'
// closed with code 1006 - would retry in 1000ms
// queued (socket not open): chat
// backoff schedule (ms): 1000, 2000, 4000, 8000, 16000, 30000
// server actually received 2 message(s)
//
// To use this against a real server, change ONE line:
//   this.socket = new FakeSocket(url);   ->   this.socket = new WebSocket(url);
// Nothing else moves. That is the payoff for writing the client against the
// interface rather than against the network.

🎯 Your Turn — Finish the Notification Client

Same fake socket, smaller client, three blanks. Each one is a decision that real WebSocket code gets wrong regularly: which field to route on, which readyState means "safe to send", and which end of the queue to flush from.

// 🎯 YOUR TURN - fill in the three blanks marked ___
// The fake socket is given. You finish the notification client.

class FakeSocket {
  constructor(url) {
    this.url = url;
    this.readyState = 0;     // 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED
    this.sent = [];
    this.onopen = null;
    this.onmessage = null;
  }
  send(data) {
    if (this.readyState !== 1) throw new Error("InvalidStateError: socket is not open");
    this.sent.push(data);
  }
  serverAccepts() { this.readyState = 1; if (this.onopen) this.onopen(); }
  serverSends(obj) { if (this.onmessage) this.onmessage({ data: JSON.stringify(obj) }); }
}

const socket = new FakeSocket("wss://example.invalid/notifications");
const outbox = [];
const handlers = {};

// 1) Route each incoming message to the right handler.
socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  const handler = handlers[msg.___];   // 👉 the field that says what kind of message it is
  if (!handler) {
    console.log("no handler for '" + msg.type + "'");
    return;
  }
  handler(msg.payload);
};

// 2) Only an OPEN socket can send. Which readyState number means OPEN?
function send(type, payload) {
  const text = JSON.stringify({ type: type, payload: payload });
  if (socket.readyState !== ___) {     // 👉 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED
    outbox.push(text);
    console.log("queued: " + type);
    return;
  }
  socket.send(text);
  console.log("sent: " + type);
}

// 3) The moment the connection opens, empty the outbox oldest-first.
socket.onopen = () => {
  console.log("open with " + outbox.length + " queued");
  while (outbox.length > 0) socket.send(___);   // 👉 outbox.shift() takes the OLDEST item
};

handlers.alert = p => console.log("ALERT: " + p.text);
handlers.receipt = p => console.log("receipt #" + p.id);

send("subscribe", { topic: "orders" });   // before the handshake finishes
socket.serverAccepts();
send("ping", {});
socket.serverSends({ type: "alert", payload: { text: "server restarting" } });
socket.serverSends({ type: "receipt", payload: { id: 7781 } });
socket.serverSends({ type: "unknown", payload: {} });
console.log("server received " + socket.sent.length + " message(s)");

// ✅ Expected output once the blanks are filled:
// queued: subscribe
// open with 1 queued
// sent: ping
// ALERT: server restarting
// receipt #7781
// no handler for 'unknown'
// server received 2 message(s)
//
// If you get "InvalidStateError: socket is not open", blank 2 is wrong: the
// code tried to send down a socket that had not finished connecting.
// If you use outbox.pop() in blank 3 the messages come out newest-first,
// which is how chat apps end up showing replies before questions.

Heartbeat (Ping/Pong) System

Every WebSocket system needs a heartbeat to detect dead connections. Clients ping periodically; if no pong returns, the connection is considered dead.

// Heartbeat (Ping/Pong) System for Dead Connection Detection

class HeartbeatWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.pingInterval = options.pingInterval || 30000;
    this.pongTimeout = options.pongTimeout || 5000;
    
    this.socket = null;
    this.pingTimer = null;
    this.pongTimer = null;
    this.isAlive = false;
    
    this.connect();
  }
  
  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onopen = () => {
      console.log("✅ Connected - starting heartbeat");
      this.isAlive = true;
      this.startHeartbeat();
    };
    
    this.socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      
      if (data.type === "pong") {
        console.log("💓 Pong received - connection alive");
        this.isAlive = true;
        this.clearPongTimeout();
      } else {
        // Handle other messages
        console.log("Message:", data);
      }
    };
    
    this.socket.onclose = () => {
      console.log("❌ Disconnected - stopping heartbeat");
      this.stopHeartbeat();
      this.scheduleReconnect();
    };
  }
  
  startHeartbeat() {
    this.pingTimer = setInterval(() => {
      if (this.socket?.readyState === WebSocket.OPEN) {
        console.log("💓 Sending ping...");
        this.socket.send(JSON.stringify({ type: "ping" }));
        
        // Set timeout for pong response
        this.pongTimer = setTimeout(() => {
          console.log("⚠️ Pong timeout - connection might be dead");
          this.isAlive = false;
          this.socket.close();
        }, this.pongTimeout);
      }
    }, this.pingInterval);
  }
  
  stopHeartbeat() {
    if (this.pingTimer) {
      clearInterval(this.pingTimer);
      this.pingTimer = null;
    }
    this.clearPongTimeout();
  }
  
  clearPongTimeout() {
    if (this.pongTimer) {
      clearTimeout(this.pongTimer);
      this.pongTimer = null;
    }
  }
  
  scheduleReconnect() {
    console.log("Reconnecting in 3 seconds...");
    setTimeout(() => this.connect(), 3000);
  }
  
  send(message) {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(message));
    }
  }
}

// Demo
console.log("Heartbeat WebSocket Demo:");
console.log("- Ping sent every 30 seconds");
console.log("- Connection closed if no pong within 5 seconds");
console.log("- Auto-reconnect on connection loss");

// Simulate heartbeat flow
console.log("\n📡 Simulated heartbeat flow:");
console.log("1. Client sends: { type: 'ping' }");
console.log("2. Server responds: { type: 'pong' }");
console.log("3. If no pong → connection considered dead");

Presence System (Online/Offline Tracking)

Presence tracking shows who's online. It requires careful handling because disconnects aren't always clean (network loss, device sleep, etc.).

// Presence System (Online/Offline Tracking)

class PresenceManager {
  constructor() {
    this.users = new Map();
    this.listeners = [];
  }
  
  // Track user coming online
  userJoined(userId, metadata = {}) {
    const user = {
      id: userId,
      status: "online",
      joinedAt: Date.now(),
      lastSeen: Date.now(),
      ...metadata
    };
    
    this.users.set(userId, user);
    this.notify("join", user);
    
    console.log(`✅ ${userId} joined (total: ${this.users.size})`);
    return user;
  }
  
  // Track user going offline
  userLeft(userId) {
    const user = this.users.get(userId);
    if (user) {
      user.status = "offline";
      user.leftAt = Date.now();
      this.users.delete(userId);
      this.notify("leave", user);
      
      console.log(`❌ ${userId} left (total: ${this.users.size})`);
    }
  }
  
  // Update last seen (from heartbeat)
  updateLastSeen(userId) {
    const user = this.users.get(userId);
    if (user) {
      user.lastSeen = Date.now();
    }
  }
  
  // Check for stale connections (no heartbeat)
  cleanupStaleUsers(maxAge = 60000) {
    const now = Date.now();
    const stale = [];
    
    this.users.forEach((user, id) => {
      if (now - user.lastSeen > maxAge) {
        stale.push(id);
      }
    });
    
    stale.forEach(id => {
      console.log(`⚠️ Removing stale user: ${id}`);
      this.userLeft(id);
    });
    
    return stale;
  }
  
  // Get all online users
  getOnlineUsers() {
    return Array.from(this.users.values());
  }
  
  // Subscribe to presence changes
  onPresenceChange(callback) {
    this.listeners.push(callback);
  }
  
  notify(event, user) {
    this.listeners.forEach(cb => cb(event, user));
  }
}

// Usage
const presence = new PresenceManager();

// Subscribe to changes
presence.onPresenceChange((event, user) => {
  console.log(`Presence event: ${event}`, user.id);
});

// Simulate users joining
presence.userJoined("user_1", { name: "Alice" });
presence.userJoined("user_2", { name: "Bob" });
presence.userJoined("user_3", { name: "Charlie" });

console.log("\nOnline users:", presence.getOnlineUsers().map(u => u.id));

// Simulate user leaving
presence.userLeft("user_2");

console.log("\nOnline users:", presence.getOnlineUsers().map(u => u.id));

Rooms & Channels

Real apps need grouping: chat rooms, game lobbies, per-user channels. Rooms allow targeted messaging instead of broadcasting to everyone.

Reliable Delivery with ACKs

WebSockets do NOT guarantee message delivery. For critical messages, implement ACKs (acknowledgments) and retry logic.

🎯 Mini-Challenge — At-Least-Once Delivery

Now build the acknowledgement loop yourself, against the same fake socket. The rule is simple and it is what "at-least-once delivery" means in practice: a message is not finished when you send it, it is finished when the server says it arrived. Anything still unconfirmed gets sent again.

Only a brief and the test script below — no starter logic. The server acknowledges just one of the three messages, so your resend must pick out exactly the other two, in order.

// 🎯 MINI-CHALLENGE: at-least-once delivery with ACKs
//
// The internet loses messages. A production client therefore keeps every
// message it has sent until the server confirms it, and resends the rest.
//
// The fake socket below is given, and already open. You write four things:
//
// 1. An object  pending  mapping id -> text, and a counter that starts at 1.
// 2. sendReliable(text):  take the next id, save pending[id] = text, then
//    socket.send(JSON.stringify({ id: id, text: text })).
// 3. socket.onmessage:  parse the JSON. If msg.type === "ack",
//    delete pending[msg.id] - that message is safely delivered.
// 4. resendUnacked():  for every id still left in pending, print
//      "resending #" + id + ": " + pending[id]

class FakeSocket {
  constructor(url) { this.url = url; this.readyState = 0; this.sent = []; this.onmessage = null; }
  send(data) {
    if (this.readyState !== 1) throw new Error("InvalidStateError: socket is not open");
    this.sent.push(data);
  }
  serverAccepts() { this.readyState = 1; }
  serverSends(obj) { if (this.onmessage) this.onmessage({ data: JSON.stringify(obj) }); }
}

const socket = new FakeSocket("wss://example.invalid/chat");
socket.serverAccepts();

// your code here

sendReliable("hello");
sendReliable("how are you");
sendReliable("bye");
socket.serverSends({ type: "ack", id: 2 });     // only the middle one is confirmed
console.log("still waiting on " + Object.keys(pending).length + " message(s)");
resendUnacked();

// ✅ Expected output:
// still waiting on 2 message(s)
// resending #1: hello
// resending #3: bye

WebSocket Security

WebSockets must be validated like any untrusted API. Always authenticate, validate messages, sanitize input, and rate limit.

Scaling with Pub/Sub Pattern

A single server can't handle millions of users. Real systems use Redis/NATS/Kafka as a message bus to broadcast across multiple servers.

📡 WebSocket Mastery Summary

Practice quiz

How does a WebSocket differ from traditional HTTP?

  • It only works on localhost
  • It is request-response only and closes after each reply
  • It provides a persistent, two-way connection where the server can push data anytime
  • It cannot send text

Answer: It provides a persistent, two-way connection where the server can push data anytime. Unlike HTTP's request-then-response model, a WebSocket keeps a persistent, bidirectional connection open so both sides can send messages anytime.

Which URL scheme is used for a secure WebSocket connection?

  • wss://
  • https://
  • ftp://
  • ws-secure://

Answer: wss://. wss:// is the secure WebSocket scheme (the WebSocket equivalent of https://).

Which event fires when a WebSocket connection is successfully established?

  • 'message'
  • 'close'
  • 'error'
  • 'open'

Answer: 'open'. The 'open' event fires when the connection is established; 'message' fires for incoming data.

Where do you read the data of an incoming WebSocket message?

  • event.body
  • event.data
  • event.payload
  • event.result

Answer: event.data. In the 'message' event handler, the received data is on event.data.

In the lesson, what does socket.readyState value 1 (OPEN) mean?

  • The connection is open and ready to send
  • The connection is closing
  • The connection is connecting
  • The connection is closed

Answer: The connection is open and ready to send. readyState 1 is OPEN (0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED).

Why does the reconnection example use exponential backoff?

  • To send messages faster
  • To encrypt the data
  • To space out reconnect attempts with increasing delays instead of hammering the server
  • To avoid using JSON

Answer: To space out reconnect attempts with increasing delays instead of hammering the server. Exponential backoff increases the delay between reconnect attempts, avoiding overwhelming a struggling server.

What is the purpose of a heartbeat (ping/pong) system?

  • To compress messages
  • To detect dead connections when no pong returns within a timeout
  • To authenticate users
  • To create rooms

Answer: To detect dead connections when no pong returns within a timeout. Clients ping periodically; if no pong arrives within the timeout, the connection is treated as dead.

Do WebSockets guarantee message delivery on their own?

  • Yes, always
  • Only over wss://
  • Only for JSON messages
  • No — for critical messages you implement ACKs and retries

Answer: No — for critical messages you implement ACKs and retries. WebSockets do NOT guarantee delivery, so reliable messaging adds acknowledgments (ACKs) and retry logic.

What is the role of rooms/channels in the lesson?

  • To encrypt messages
  • To allow targeted messaging to a group instead of broadcasting to everyone
  • To reconnect faster
  • To store messages on disk

Answer: To allow targeted messaging to a group instead of broadcasting to everyone. Rooms group users so messages can be sent to a targeted set instead of every connected client.

How do you scale WebSockets across multiple servers in the lesson?

  • Using a single giant server
  • By disabling heartbeats
  • Using a Pub/Sub message bus (like Redis) to broadcast across servers
  • By switching to HTTP polling

Answer: Using a Pub/Sub message bus (like Redis) to broadcast across servers. A Pub/Sub pattern (Redis/NATS/Kafka) lets a message published on one server reach clients connected to all servers.

Continue this course