WebSockets & Real-Time Communication

1. Introduction ⚡

Traditional HTTP follows a request-response model — the client always initiates. WebSockets flip this by opening a persistent, bidirectional connection, enabling true real-time features like live chat, notifications, and multiplayer games.

2. What are WebSockets? 🔌

A WebSocket is a full-duplex communication channel over a single TCP connection. Once established, either side can send messages at any time, without waiting for a request.

  • Persistent connection — no repeated handshakes per message.
  • Full-duplex — client and server can send simultaneously.
  • Low overhead per message compared to repeated HTTP requests.

3. HTTP vs WebSockets 🆚

AspectHTTPWebSockets
ConnectionNew per request (or Keep-Alive reuse)Single persistent connection
DirectionClient-initiated onlyBidirectional, either side
OverheadHeaders on every requestMinimal framing after handshake
Use caseTraditional request/responseReal-time, low-latency updates

4. WebSocket Protocol 📜

A WebSocket connection begins as a regular HTTP request with an Upgrade: websocket header. If the server agrees, it responds with 101 Switching Protocols, and the TCP connection is repurposed for the ws:// (or wss:// for encrypted) protocol.

Handshake
Client sends HTTP request with Upgrade: websocket
Server responds 101 Switching Protocols
Connection becomes a persistent WebSocket

5. Creating a WebSocket Server đŸ–Ĩī¸

Node has no built-in WebSocket module — the ws package is the most widely used lightweight implementation.

ws-server.js

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (socket) => {
  console.log('Client connected');
  socket.send('Welcome!');
});

6. Creating a WebSocket Client đŸ’ģ

ws-client.js

const socket = new WebSocket('ws://localhost:8080');

socket.addEventListener('open', () => {
  console.log('Connected to server');
  socket.send('Hello, server!');
});

socket.addEventListener('message', (event) => {
  console.log('Received:', event.data);
});

Information

The WebSocket constructor is available natively in browsers; in Node, use the ws package's client class instead.

7. Connection Lifecycle 🔄

connection-lifecycle.js

wss.on('connection', (socket) => {
  console.log('open');

  socket.on('message', (data) => console.log('message:', data.toString()));
  socket.on('close', (code, reason) => console.log('closed', code, reason.toString()));
  socket.on('error', (err) => console.error('error:', err.message));
});

8. Sending Messages 📤

sending-messages.js

socket.send('plain text message');
socket.send(JSON.stringify({ type: 'chat', text: 'Hello!' }));

9. Receiving Messages đŸ“Ĩ

receiving-messages.js

socket.on('message', (data) => {
  const parsed = JSON.parse(data.toString());
  console.log(parsed.type, parsed.text);
});

Tip

Incoming data arrives as a Buffer by default — call .toString() before parsing as JSON.

10. Broadcasting Messages đŸ“ĸ

Broadcasting means sending a message to every connected client, typically by iterating over the server's client set.

broadcast.js

function broadcast(wss, message) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

11. Rooms & Channels đŸšĒ

Rooms (or channels) group clients so messages can be scoped to a subset of connections — essential for multi-conversation chat apps or multiplayer game lobbies.

rooms-example.js

const rooms = new Map(); // roomName -> Set of sockets

function joinRoom(roomName, socket) {
  if (!rooms.has(roomName)) rooms.set(roomName, new Set());
  rooms.get(roomName).add(socket);
}

function broadcastToRoom(roomName, message) {
  const members = rooms.get(roomName) || [];
  for (const client of members) {
    if (client.readyState === WebSocket.OPEN) client.send(message);
  }
}

12. Heartbeats & Ping/Pong 💓

Since dropped connections aren't always detected immediately, servers use ping/pong frames to periodically verify a client is still alive.

heartbeat.js

wss.on('connection', (socket) => {
  socket.isAlive = true;
  socket.on('pong', () => (socket.isAlive = true));
});

setInterval(() => {
  wss.clients.forEach((socket) => {
    if (!socket.isAlive) return socket.terminate();
    socket.isAlive = false;
    socket.ping();
  });
}, 30000);

13. Reconnection Strategies 🔁

Network blips happen — clients should implement automatic reconnection, usually with exponential backoff to avoid hammering the server.

reconnection.js

function connectWithRetry(url, attempt = 0) {
  const socket = new WebSocket(url);

  socket.addEventListener('close', () => {
    const delay = Math.min(1000 * 2 ** attempt, 30000);
    setTimeout(() => connectWithRetry(url, attempt + 1), delay);
  });

  return socket;
}

14. Authentication 🔑

Since the WebSocket handshake is a regular HTTP request, authentication tokens are commonly passed via a query parameter or Authorization header before the upgrade completes.

ws-auth.js

const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', (req, socket, head) => {
  const token = new URL(req.url, 'http://localhost').searchParams.get('token');
  if (!isValidToken(token)) {
    socket.destroy();
    return;
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req);
  });
});

15. Authorization 🛂

Beyond confirming who a user is, authorization determines what they can do — which rooms they can join, which messages they're allowed to send or receive.

ws-authorization.js

socket.on('message', (data) => {
  const msg = JSON.parse(data.toString());
  if (msg.type === 'joinRoom' && !userCanJoin(socket.userId, msg.room)) {
    socket.send(JSON.stringify({ error: 'Not authorized for this room' }));
    return;
  }
  joinRoom(msg.room, socket);
});

16. Binary Data 💾

WebSockets support both text and binary frames — useful for transmitting images, audio chunks, or compact binary protocols.

binary-data.js

socket.on('message', (data, isBinary) => {
  if (isBinary) {
    console.log('Received binary data:', data.length, 'bytes');
  } else {
    console.log('Received text:', data.toString());
  }
});

socket.send(Buffer.from([1, 2, 3, 4]));

17. Socket.IO Overview 🧩

Socket.IO is a popular library built on top of WebSockets (with automatic fallback to HTTP long-polling), adding rooms, reconnection, and acknowledgments out of the box.

socketio-server.js

const { Server } = require('socket.io');

const io = new Server(3000);

io.on('connection', (socket) => {
  socket.join('room1');
  socket.on('chatMessage', (msg) => {
    io.to('room1').emit('chatMessage', msg);
  });
});

Information

Socket.IO is not wire-compatible with raw WebSockets — its client and server must be paired together, unlike plain ws which follows the standard protocol.

18. Server-Sent Events (SSE) đŸ“ļ

SSE provides a one-way (server-to-client) real-time channel over plain HTTP — simpler than WebSockets when the client never needs to send data back.

sse-server.js

http.createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });

  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on('close', () => clearInterval(interval));
}).listen(3000);

Tip

Use SSE for one-directional feeds like live scores or notifications; use WebSockets when the client also needs to send data.

19. Real-Time Notifications 🔔

A typical notification system pushes events to specific connected users, often keyed by user ID rather than a shared room.

notifications.js

const userSockets = new Map(); // userId -> socket

function notifyUser(userId, payload) {
  const socket = userSockets.get(userId);
  if (socket && socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify({ type: 'notification', ...payload }));
  }
}

20. Live Chat Applications đŸ’Ŧ

live-chat.js

wss.on('connection', (socket, req) => {
  socket.on('message', (data) => {
    const { room, text, user } = JSON.parse(data.toString());
    broadcastToRoom(room, JSON.stringify({ user, text, ts: Date.now() }));
  });
});

21. Multiplayer Applications 🎮

Real-time multiplayer games broadcast frequent, small state updates (positions, actions) — often over UDP-like protocols in native games, but WebSockets work well for browser-based games with moderate update rates.

multiplayer-state.js

setInterval(() => {
  const state = getWorldState();
  broadcast(wss, JSON.stringify({ type: 'stateUpdate', state }));
}, 50); // ~20 updates per second

22. Performance Optimization ⚡

  • Batch frequent small updates instead of sending a message per tiny change.
  • Use binary formats (like MessagePack) over JSON for high-frequency data.
  • Scale across processes with a shared pub/sub layer (e.g. Redis) so broadcasts reach clients connected to different server instances.
  • Tune heartbeat intervals to balance responsiveness against unnecessary network traffic.

23. Security Best Practices đŸ›Ąī¸

  • Always use wss:// (encrypted) in production, never plain ws://.
  • Validate the Origin header during the handshake to prevent unauthorized cross-site connections.
  • Authenticate before completing the upgrade, not after the connection is already open.
  • Rate-limit incoming messages per connection to prevent abuse or flooding.
  • Validate and sanitize all message payloads — never trust client-supplied data.

24. Common Mistakes âš ī¸

  • Forgetting to check readyState before calling .send(), causing errors on closed sockets.
  • Not implementing heartbeats, leaving dead connections lingering indefinitely.
  • Storing room/user membership only in memory on a single server instance, breaking under horizontal scaling.
  • Trusting client-sent user IDs instead of deriving identity from an authenticated session.
  • Not handling the 'error' event, leading to unhandled exceptions on connection issues.

25. Frequently Asked Questions ❓

Question

Should I use raw ws or Socket.IO?

Answer

Use ws for a lightweight, standards-compliant WebSocket implementation. Choose Socket.IO if you want built-in rooms, automatic reconnection, and fallback transports out of the box.

Question

Can I use WebSockets behind a load balancer?

Answer

Yes, but you need sticky sessions (so a client stays connected to the same server instance) or a shared pub/sub layer to broadcast messages across instances.

Question

When should I use SSE instead of WebSockets?

Answer

Use SSE when data only flows server to client — it's simpler, works over plain HTTP, and reconnects automatically in browsers.

26. Summary 📝

Summary

WebSockets enable persistent, bidirectional communication ideal for chat, notifications, and multiplayer applications — built on an HTTP handshake but operating independently afterward. Concepts like heartbeats, rooms, and reconnection strategies keep real-time systems resilient, while libraries like ws and Socket.IO — or the simpler one-directional Server-Sent Events — cover most real-world real-time needs.