06-websocket.md

Module 06: WebSocket & Real-Time Communication

Goal: Understand WebSocket protocol, build real-time features, and learn Socket.IO. Time: 2 days of focused study Prerequisites: Module 01-04


Table of Contents

  1. The Problem with HTTP for Real-Time
  2. What is WebSocket?
  3. WebSocket Protocol — Under the Hood
  4. The ws Library — Raw WebSocket
  5. Socket.IO — Feature-Rich WebSocket
  6. Real-World Patterns
  7. Authentication with WebSocket
  8. Scaling WebSocket Servers
  9. Server-Sent Events (SSE) — Alternative
  10. Practice: Build a Chat App
  11. Interview Questions

1. The Problem with HTTP

HTTP is a request-response protocol — the client asks, the server responds. The server can NEVER initiate communication.

HTTP (Half-Duplex): Client ──── Request ───► Server Client ◄─── Response ─── Server Problem: If something happens on the server, the client doesn't know until it asks. Workarounds (all have problems): 1. Short Polling (dumb): Client asks every 1 second: "Any updates?" Server: "No." "No." "No." "Yes! Here's the update." Problem: Wastes bandwidth, high latency, 90% of requests return nothing 2. Long Polling (smarter): Client asks: "Any updates?" Server holds the connection open until an update is available (or timeout) Client immediately asks again after getting a response. Problem: Each response requires a new HTTP connection, HTTP overhead per message 3. Server-Sent Events (good for one-way): Server pushes events to client over a long-lived HTTP connection Problem: One-directional (server → client only), no binary data

2. What is WebSocket?

WebSocket is a full-duplex protocol — both client and server can send messages at any time, independently.

WebSocket (Full-Duplex): Client ◄──── Messages ────► Server ✅ Both sides can send at any time ✅ Single persistent TCP connection ✅ Very low overhead per message (2-14 bytes vs 100+ for HTTP) ✅ Real-time bidirectional communication

HTTP vs WebSocket Comparison

Feature │ HTTP │ WebSocket ─────────────────┼───────────────────┼────────────────── Direction │ Client → Server │ Both directions Connection │ New per request │ Persistent Overhead │ ~100 bytes/msg │ ~2-14 bytes/msg State │ Stateless │ Stateful Best for │ CRUD, REST APIs │ Real-time apps Protocol │ http:// / https://│ ws:// / wss://

3. WebSocket Protocol

The Handshake

WebSocket starts with an HTTP upgrade request:

Client → Server (HTTP Upgrade Request): GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket ← "I want to upgrade to WebSocket" Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZQ== ← Random base64 string Sec-WebSocket-Version: 13 Server → Client (101 Switching Protocols): HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ← Proof of receipt After this handshake, the connection is upgraded from HTTP to WebSocket. The TCP connection stays open for bidirectional messaging.

Frame Format

WebSocket messages are sent in "frames": 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+-------------------------------+ | Payload Data | +---------------------------------------------------------------+ Opcodes: 0x1 = text frame 0x2 = binary frame 0x8 = close 0x9 = ping 0xA = pong Overhead comparison: HTTP: ~100-800 bytes per request (headers) WebSocket: ~2-14 bytes per frame (just the frame header)

4. The ws Library — Raw WebSocket

ws is a simple, fast WebSocket library for Node.js.

npm install ws

Basic Server

const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 8080 }); wss.on('connection', (ws, req) => { console.log('Client connected from:', req.socket.remoteAddress); // Send message to client ws.send(JSON.stringify({ type: 'welcome', message: 'Hello!' })); // Receive messages from client ws.on('message', (data) => { const message = JSON.parse(data.toString()); console.log('Received:', message); // Echo back ws.send(JSON.stringify({ type: 'echo', data: message })); }); // Client disconnected ws.on('close', (code, reason) => { console.log(`Client disconnected: ${code} - ${reason}`); }); // Error handling ws.on('error', (err) => { console.error('WebSocket error:', err); }); // Ping/Pong for connection health check ws.on('pong', () => { ws.isAlive = true; }); }); // Health check interval — detect dead connections const interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) { console.log('Terminating dead connection'); return ws.terminate(); } ws.isAlive = false; ws.ping(); // Client auto-responds with pong }); }, 30000); // Check every 30 seconds wss.on('close', () => clearInterval(interval)); console.log('WebSocket server running on ws://localhost:8080');

Basic Client (Node.js)

const WebSocket = require('ws'); const ws = new WebSocket('ws://localhost:8080'); ws.on('open', () => { console.log('Connected to server'); ws.send(JSON.stringify({ type: 'chat', content: 'Hello, server!' })); }); ws.on('message', (data) => { const message = JSON.parse(data.toString()); console.log('From server:', message); }); ws.on('close', () => console.log('Disconnected')); ws.on('error', (err) => console.error('Error:', err));

Basic Client (Browser)

// Browser has built-in WebSocket API — no library needed! const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { console.log('Connected'); ws.send(JSON.stringify({ type: 'chat', content: 'Hello!' })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); console.log('Received:', message); }; ws.onclose = (event) => { console.log(`Disconnected: ${event.code} ${event.reason}`); }; ws.onerror = (error) => { console.error('Error:', error); };

Broadcasting to All Clients

const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 8080 }); // Broadcast to ALL connected clients function broadcast(data, exclude = null) { const message = JSON.stringify(data); wss.clients.forEach((client) => { if (client !== exclude && client.readyState === WebSocket.OPEN) { client.send(message); } }); } wss.on('connection', (ws) => { ws.on('message', (data) => { const message = JSON.parse(data.toString()); if (message.type === 'chat') { // Broadcast to all OTHER clients broadcast({ type: 'chat', from: message.from, content: message.content, timestamp: Date.now(), }, ws); // Exclude sender } }); });

Integrating with Express

const express = require('express'); const http = require('http'); const { WebSocketServer } = require('ws'); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); // Share the same server! // Express routes app.get('/api/status', (req, res) => { res.json({ status: 'ok', connections: wss.clients.size, }); }); // WebSocket connections wss.on('connection', (ws, req) => { // req.url can be used for routing: ws://localhost:3000/chat console.log(`WebSocket connection to ${req.url}`); ws.on('message', (data) => { // Handle messages }); }); server.listen(3000, () => { console.log('HTTP + WebSocket server on port 3000'); });

5. Socket.IO — Feature-Rich WebSocket

Socket.IO adds reliability features on top of WebSocket:

Feature │ Raw WebSocket │ Socket.IO ──────────────────┼───────────────┼────────────────── Auto-reconnection │ ❌ Manual │ ✅ Built-in Rooms/namespaces │ ❌ Manual │ ✅ Built-in Broadcasting │ ❌ Manual │ ✅ Built-in Fallbacks │ ❌ WebSocket │ ✅ HTTP long-polling Acknowledgements │ ❌ Manual │ ✅ Built-in Binary support │ ✅ Yes │ ✅ Yes Middleware │ ❌ No │ ✅ Yes

Server Setup

// npm install socket.io const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: 'http://localhost:3000', methods: ['GET', 'POST'], credentials: true, }, pingTimeout: 60000, pingInterval: 25000, }); // Connection event io.on('connection', (socket) => { console.log(`User connected: ${socket.id}`); // Listen for events from this client socket.on('chat:message', (data) => { console.log('Message:', data); // Emit to ALL clients (including sender) io.emit('chat:message', { ...data, id: Date.now(), timestamp: new Date(), }); }); // Emit to all EXCEPT sender socket.on('user:typing', (data) => { socket.broadcast.emit('user:typing', data); }); // Acknowledgement — client confirms receipt socket.on('message:read', (data, callback) => { // Process the event markMessageAsRead(data.messageId); // Send acknowledgement back to client callback({ status: 'ok', readAt: new Date() }); }); // Disconnect socket.on('disconnect', (reason) => { console.log(`User disconnected: ${socket.id}${reason}`); }); }); server.listen(3000);

Client Setup

// npm install socket.io-client const { io } = require('socket.io-client'); // Or in browser: <script src="/socket.io/socket.io.js"></script> const socket = io('http://localhost:3000', { reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, auth: { token: 'user-jwt-token', // Send auth data on connection }, }); socket.on('connect', () => { console.log('Connected:', socket.id); }); socket.on('chat:message', (message) => { console.log('New message:', message); }); // Send message socket.emit('chat:message', { content: 'Hello!', room: 'general', }); // Send with acknowledgement socket.emit('message:read', { messageId: 123 }, (response) => { console.log('Server acknowledged:', response); }); socket.on('disconnect', (reason) => { console.log('Disconnected:', reason); });

Rooms — Grouping Clients

io.on('connection', (socket) => { // Join a room socket.on('room:join', (roomName) => { socket.join(roomName); console.log(`${socket.id} joined room: ${roomName}`); // Notify others in the room socket.to(roomName).emit('room:userJoined', { userId: socket.id, room: roomName, }); }); // Leave a room socket.on('room:leave', (roomName) => { socket.leave(roomName); socket.to(roomName).emit('room:userLeft', { userId: socket.id }); }); // Send message to a specific room socket.on('room:message', ({ room, content }) => { io.to(room).emit('chat:message', { from: socket.id, content, room, timestamp: Date.now(), }); }); // Emit to a specific user socket.on('private:message', ({ targetId, content }) => { io.to(targetId).emit('private:message', { from: socket.id, content, }); }); }); // Emitting cheatsheet: // io.emit(event, data) → ALL clients // socket.emit(event, data) → ONLY this client // socket.broadcast.emit(event, data) → ALL except this client // io.to('room').emit(event, data) → ALL in room // socket.to('room').emit(event, data) → ALL in room except sender // io.to(socketId).emit(event, data) → Specific client

Namespaces — Separate Communication Channels

// Default namespace is '/' const io = new Server(server); // Custom namespaces const chatNsp = io.of('/chat'); const adminNsp = io.of('/admin'); const notificationsNsp = io.of('/notifications'); chatNsp.on('connection', (socket) => { console.log('User connected to /chat'); // Only handles chat-related events }); adminNsp.on('connection', (socket) => { // Verify admin access if (socket.handshake.auth.role !== 'admin') { socket.disconnect(); return; } console.log('Admin connected to /admin'); }); notificationsNsp.on('connection', (socket) => { console.log('User connected to /notifications'); }); // Client connects to specific namespace: const chatSocket = io('http://localhost:3000/chat'); const adminSocket = io('http://localhost:3000/admin', { auth: { role: 'admin', token: 'xxx' } });

Socket.IO Middleware

// Authentication middleware io.use((socket, next) => { const token = socket.handshake.auth.token; if (!token) { return next(new Error('Authentication required')); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); socket.user = decoded; // Attach user info to socket next(); } catch (err) { next(new Error('Invalid token')); } }); // Logging middleware io.use((socket, next) => { console.log(`Connection attempt from: ${socket.handshake.address}`); next(); }); // Per-namespace middleware chatNsp.use(chatAuthMiddleware); // Handle middleware errors on client: socket.on('connect_error', (err) => { console.log('Connection error:', err.message); // "Authentication required" or "Invalid token" });

6. Real-World Patterns

1. Presence System (Online/Offline Status)

const onlineUsers = new Map(); // userId → Set of socketIds io.on('connection', (socket) => { const userId = socket.user.id; // Track online status if (!onlineUsers.has(userId)) { onlineUsers.set(userId, new Set()); } onlineUsers.get(userId).add(socket.id); // Notify others that user is online socket.broadcast.emit('user:online', { userId }); // Send current online users list socket.emit('users:online', Array.from(onlineUsers.keys())); socket.on('disconnect', () => { const userSockets = onlineUsers.get(userId); userSockets?.delete(socket.id); // Only mark as offline if no more connections (user might have multiple tabs) if (!userSockets || userSockets.size === 0) { onlineUsers.delete(userId); socket.broadcast.emit('user:offline', { userId }); } }); });

2. Typing Indicator

io.on('connection', (socket) => { let typingTimeout; socket.on('typing:start', ({ room }) => { socket.to(room).emit('typing:start', { userId: socket.user.id, name: socket.user.name, }); // Auto-stop after 3 seconds of no typing events clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { socket.to(room).emit('typing:stop', { userId: socket.user.id }); }, 3000); }); socket.on('typing:stop', ({ room }) => { clearTimeout(typingTimeout); socket.to(room).emit('typing:stop', { userId: socket.user.id }); }); });

3. Real-Time Notifications

// Send notification to specific user from anywhere in your app function sendNotification(userId, notification) { const userSockets = onlineUsers.get(userId); if (userSockets) { userSockets.forEach(socketId => { io.to(socketId).emit('notification', { id: crypto.randomUUID(), ...notification, createdAt: new Date(), read: false, }); }); } // Also save to database for offline users saveNotificationToDB(userId, notification); } // Usage from Express route: app.post('/api/orders', async (req, res) => { const order = await Order.create(req.body); // Send real-time notification sendNotification(order.userId, { type: 'order_created', title: 'Order Confirmed', message: `Your order #${order.id} has been placed.`, }); res.status(201).json(order); });

4. Live Collaborative Editing

io.on('connection', (socket) => { socket.on('document:join', (docId) => { socket.join(`doc:${docId}`); // Send current document state to new user const doc = documents.get(docId); socket.emit('document:state', doc); }); socket.on('document:change', ({ docId, change }) => { // Apply change to document applyChange(docId, change); // Broadcast change to all OTHER editors socket.to(`doc:${docId}`).emit('document:change', { change, userId: socket.user.id, timestamp: Date.now(), }); }); socket.on('cursor:move', ({ docId, position }) => { socket.to(`doc:${docId}`).emit('cursor:move', { userId: socket.user.id, position, }); }); });

7. Authentication with WebSocket

With ws Library

const { WebSocketServer } = require('ws'); const jwt = require('jsonwebtoken'); const url = require('url'); const wss = new WebSocketServer({ server }); wss.on('connection', (ws, req) => { // Method 1: Token in query string // ws://localhost:8080?token=eyJhbG... const parsedUrl = new URL(req.url, `http://${req.headers.host}`); const token = parsedUrl.searchParams.get('token'); // Method 2: Token in custom header (from Node.js clients) // const token = req.headers['authorization']?.split(' ')[1]; // Method 3: Token in cookie // const cookies = parseCookies(req.headers.cookie); // const token = cookies.accessToken; try { const user = jwt.verify(token, process.env.JWT_SECRET); ws.user = user; ws.send(JSON.stringify({ type: 'auth:success', user })); } catch (err) { ws.send(JSON.stringify({ type: 'auth:failed', error: 'Invalid token' })); ws.close(4001, 'Unauthorized'); return; } ws.on('message', (data) => { console.log(`Message from ${ws.user.name}:`, data.toString()); }); });

Verify on Upgrade (Before Connection Established)

const server = http.createServer(app); const wss = new WebSocketServer({ noServer: true }); // Handle upgrade manually — verify BEFORE accepting connection server.on('upgrade', (request, socket, head) => { const token = new URL(request.url, 'http://localhost').searchParams.get('token'); try { const user = jwt.verify(token, process.env.JWT_SECRET); wss.handleUpgrade(request, socket, head, (ws) => { ws.user = user; wss.emit('connection', ws, request); }); } catch (err) { socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); } });

8. Scaling WebSocket Servers

The Problem

With 1 server, broadcasting works fine: Server 1: [User A, User B, User C] broadcast() → sends to A, B, C ✅ With multiple servers (load balanced), broadcasting breaks: Server 1: [User A, User B] Server 2: [User C, User D] User A sends message on Server 1 broadcast() on Server 1 → sends to A, B only ❌ User C and D on Server 2 never get the message!

Solution: Redis Adapter (Pub/Sub)

// npm install @socket.io/redis-adapter redis const { createAdapter } = require('@socket.io/redis-adapter'); const { createClient } = require('redis'); const pubClient = createClient({ url: 'redis://localhost:6379' }); const subClient = pubClient.duplicate(); await Promise.all([pubClient.connect(), subClient.connect()]); io.adapter(createAdapter(pubClient, subClient)); // Now io.emit() and socket.to('room').emit() work across ALL servers! // The Redis adapter uses pub/sub to relay messages between servers. /* Architecture: User A ──► Server 1 ──┐ User B ──► Server 1 │ ├──► Redis (Pub/Sub) ──► All Servers User C ──► Server 2 │ User D ──► Server 2 ──┘ When Server 1 emits a message: 1. Server 1 sends to its local clients (A, B) 2. Server 1 publishes to Redis 3. Redis broadcasts to all other servers 4. Server 2 receives from Redis and sends to its clients (C, D) */

Sticky Sessions for WebSocket

Problem: Socket.IO uses HTTP long-polling initially, then upgrades to WebSocket. The initial HTTP requests and the upgrade MUST hit the same server. Solution: Sticky sessions — ensure all requests from one client go to one server. Nginx config: upstream io_nodes { ip_hash; # Sticky sessions based on IP server 127.0.0.1:3001; server 127.0.0.1:3002; server 127.0.0.1:3003; } server { location /socket.io/ { proxy_pass http://io_nodes; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }

9. Server-Sent Events (SSE)

SSE is a simpler alternative when you only need server → client communication.

// Server app.get('/api/events', (req, res) => { // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); // Send events const sendEvent = (event, data) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }; // Send initial data sendEvent('connected', { message: 'Connected to SSE' }); // Send periodic updates const interval = setInterval(() => { sendEvent('heartbeat', { timestamp: Date.now() }); }, 5000); // Listen for new data (e.g., from database changes) const handler = (notification) => { sendEvent('notification', notification); }; eventBus.on('notification', handler); // Cleanup on disconnect req.on('close', () => { clearInterval(interval); eventBus.off('notification', handler); }); }); // Client (Browser — built-in API) const source = new EventSource('/api/events'); source.addEventListener('notification', (event) => { const data = JSON.parse(event.data); console.log('Notification:', data); }); source.addEventListener('heartbeat', (event) => { console.log('Heartbeat:', JSON.parse(event.data)); }); source.onerror = () => { console.log('SSE connection error — will auto-reconnect'); };

When to Use What

SSE (Server-Sent Events): ✅ Server → Client only (notifications, live feeds, stock prices) ✅ Auto-reconnection built-in ✅ Simpler than WebSocket ✅ Works over HTTP (no protocol upgrade) ❌ One-directional only ❌ No binary data WebSocket: Bidirectional (chat, games, collaboration) ✅ Binary data support ✅ Lower latency ❌ More complex ❌ Need manual reconnection (or Socket.IO) Long Polling: ✅ Works everywhere (even behind strict firewalls) ❌ Higher latency ❌ More server resources ❌ Complex to implement well

10. Practice: Build a Chat App

// Complete chat application with rooms, typing indicators, and history // server.js const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const jwt = require('jsonwebtoken'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*' } }); // In-memory storage (use database in production) const messages = new Map(); // room → messages[] const onlineUsers = new Map(); // userId → { socketId, name, room } // Auth middleware io.use((socket, next) => { const token = socket.handshake.auth.token; try { socket.user = jwt.verify(token, process.env.JWT_SECRET); next(); } catch (err) { next(new Error('Authentication failed')); } }); io.on('connection', (socket) => { console.log(`${socket.user.name} connected`); // ---- JOIN ROOM ---- socket.on('room:join', (room) => { // Leave previous room const prevRoom = onlineUsers.get(socket.user.id)?.room; if (prevRoom) { socket.leave(prevRoom); socket.to(prevRoom).emit('user:left', { name: socket.user.name }); } // Join new room socket.join(room); onlineUsers.set(socket.user.id, { socketId: socket.id, name: socket.user.name, room, }); // Send room history const history = messages.get(room) || []; socket.emit('room:history', history.slice(-50)); // Last 50 messages // Notify room socket.to(room).emit('user:joined', { name: socket.user.name }); // Send online users in room const roomUsers = Array.from(onlineUsers.values()) .filter(u => u.room === room) .map(u => u.name); io.to(room).emit('room:users', roomUsers); }); // ---- SEND MESSAGE ---- socket.on('message:send', ({ content }, callback) => { const userInfo = onlineUsers.get(socket.user.id); if (!userInfo?.room) return; const message = { id: Date.now().toString(), userId: socket.user.id, name: socket.user.name, content, room: userInfo.room, timestamp: new Date().toISOString(), }; // Store message if (!messages.has(userInfo.room)) messages.set(userInfo.room, []); messages.get(userInfo.room).push(message); // Broadcast to room io.to(userInfo.room).emit('message:new', message); // Acknowledge callback?.({ status: 'sent', id: message.id }); }); // ---- TYPING ---- socket.on('typing:start', () => { const userInfo = onlineUsers.get(socket.user.id); if (userInfo?.room) { socket.to(userInfo.room).emit('typing:update', { userId: socket.user.id, name: socket.user.name, isTyping: true, }); } }); socket.on('typing:stop', () => { const userInfo = onlineUsers.get(socket.user.id); if (userInfo?.room) { socket.to(userInfo.room).emit('typing:update', { userId: socket.user.id, name: socket.user.name, isTyping: false, }); } }); // ---- DISCONNECT ---- socket.on('disconnect', () => { const userInfo = onlineUsers.get(socket.user.id); if (userInfo?.room) { socket.to(userInfo.room).emit('user:left', { name: socket.user.name }); // Update room users list onlineUsers.delete(socket.user.id); const roomUsers = Array.from(onlineUsers.values()) .filter(u => u.room === userInfo.room) .map(u => u.name); io.to(userInfo.room).emit('room:users', roomUsers); } }); }); server.listen(3000, () => console.log('Chat server on :3000'));

11. Interview Questions

Q1: What is WebSocket? How is it different from HTTP?

Answer: WebSocket is a full-duplex communication protocol that provides persistent, bidirectional communication over a single TCP connection. Unlike HTTP (request-response, new connection each time), WebSocket maintains an open connection where both client and server can send messages at any time. It starts with an HTTP upgrade handshake, then switches to the WebSocket protocol with much lower overhead per message (2-14 bytes vs 100+ for HTTP).

Q2: When would you use WebSocket vs Server-Sent Events vs Long Polling?

Answer: WebSocket for bidirectional real-time (chat, games, collaboration). SSE for server-to-client only (notifications, live feeds) — simpler, auto-reconnects, works over HTTP. Long polling as a fallback when WebSocket is blocked (corporate firewalls) — higher latency and resource usage.

Q3: How would you scale a WebSocket server?

Answer: Use a Redis adapter (pub/sub) to relay messages between server instances. Configure sticky sessions at the load balancer (Nginx ip_hash) so Socket.IO's HTTP polling requests reach the same server. Each server handles its local connections and publishes events to Redis; other servers subscribe and forward to their local clients.

Q4: How do you handle authentication with WebSocket?

Answer: Send JWT in the connection handshake (query parameter, auth object, or cookie). Verify the token before accepting the connection (using the upgrade event or Socket.IO middleware). For the ws library, verify in the connection handler or intercept the HTTP upgrade event. For Socket.IO, use io.use() middleware.

Q5: What are WebSocket rooms?

Answer: Rooms are a server-side concept (Socket.IO) for grouping sockets. A socket can join multiple rooms, and you can emit events to all sockets in a room. Use cases: chat rooms, game lobbies, document collaboration, sending updates to users subscribed to specific topics.


Next Module: 07 - Redis — Caching, pub/sub, rate limiting, and real-time data storage.