Chapter 58 โ WebSockets & Real-Time
๐ Definition
WebSocket is a protocol that upgrades an HTTP connection into a persistent, full-duplex channel, so the server can push data to the client at any time. Socket.IO is a library on top of WebSocket that adds automatic reconnection, fallbacks, rooms, namespaces, acknowledgements and multi-server scaling.
๐ Choosing a Real-Time Technique
| Technique | Direction | Use when |
|---|---|---|
Polling (setInterval + fetch) |
Client pulls | Updates every 30 s+ are fine; simplest |
| Long polling | Client pulls, server holds | Legacy fallback |
| Server-Sent Events (SSE) | Server โ client only, over HTTP | Notifications, live scores, streaming AI tokens |
| WebSocket / Socket.IO | Both ways | Chat, collaborative editing, multiplayer, live tracking |
๐ป Code Example โ Socket.IO Server With Express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
import jwt from "jsonwebtoken";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: "https://app.example.com", credentials: true },
});
// Authenticate the socket once, during the handshake
io.use((socket, next) => {
try {
const token = socket.handshake.auth.token;
socket.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
next(new Error("Unauthorized"));
}
});
io.on("connection", (socket) => {
socket.join(`user:${socket.user.id}`); // personal room for notifications
socket.on("chat:join", (roomId) => socket.join(`chat:${roomId}`));
socket.on("chat:message", async ({ roomId, text }, ack) => {
if (!text?.trim()) return ack({ ok: false, error: "Empty message" });
const msg = await Message.create({ roomId, text, from: socket.user.id });
socket.to(`chat:${roomId}`).emit("chat:message", msg); // everyone in room except sender
ack({ ok: true, id: msg._id }); // acknowledgement to sender
});
socket.on("typing", (roomId) => socket.to(`chat:${roomId}`).volatile.emit("typing", socket.user.id));
socket.on("disconnect", (reason) => console.log("left", socket.user.id, reason));
});
httpServer.listen(4000);๐ป Code Example โ Emitting From a REST Route
app.patch("/api/orders/:id/status", auth, async (req, res) => {
const order = await Order.findByIdAndUpdate(req.params.id, { status: req.body.status }, { new: true });
io.to(`user:${order.userId}`).emit("order:status", { id: order._id, status: order.status });
res.json(order);
});๐ป Code Example โ React Client
import { io } from "socket.io-client";
export function useSocket(token) {
const [socket, setSocket] = useState(null);
useEffect(() => {
const s = io(import.meta.env.VITE_API_URL, { auth: { token } });
setSocket(s);
return () => s.disconnect(); // avoid duplicate connections
}, [token]);
return socket;
}
function ChatRoom({ roomId, socket }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
if (!socket) return;
socket.emit("chat:join", roomId);
const onMessage = (msg) => setMessages((prev) => [...prev, msg]);
socket.on("chat:message", onMessage);
return () => socket.off("chat:message", onMessage); // remove THIS listener only
}, [socket, roomId]);
const send = (text) =>
socket.emit("chat:message", { roomId, text }, (res) => {
if (!res.ok) alert(res.error);
});
// โฆ
}Classic bug: forgetting socket.off in cleanup โ every re-render or StrictMode double-mount adds another listener, and messages appear 2ร, 3รโฆ
๐ป Code Example โ Scaling Across Servers With Redis
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// io.to("chat:42").emit(...) now reaches clients connected to ANY instance.Also enable sticky sessions on the load balancer if clients may fall back to HTTP long-polling, or force transports: ["websocket"].
๐ป Code Example โ Server-Sent Events (No Library)
app.get("/api/notifications/stream", auth, (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const send = (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
const heartbeat = setInterval(() => res.write(": ping\n\n"), 25_000);
const unsubscribe = notifications.subscribe(req.user.id, send);
req.on("close", () => { clearInterval(heartbeat); unsubscribe(); });
});// Browser
const es = new EventSource("/api/notifications/stream", { withCredentials: true });
es.onmessage = (e) => showToast(JSON.parse(e.data)); // auto-reconnects๐ Production Concerns
- Authentication: verify on handshake; re-check permissions when joining rooms (can this user join
chat:42?). - Missed messages: clients that reconnect must fetch history (
GET /messages?after=<lastId>); sockets are not a durable queue. - Message ordering and duplicates: include IDs and timestamps; de-duplicate on the client.
- Heartbeats: Socket.IO pings automatically; proxies (Nginx, ALB) need idle timeouts above the ping interval and
Upgradeheaders configured. - Rate limiting per socket to stop spam.
- Persist first, then emit, so a crash never shows a message that was not saved.
๐ Real-World Use
- Chat and support widgets, live order tracking, delivery-partner location, stock tickers, collaborative docs, live dashboards, multiplayer games.
๐ฏ Likely Interview Questions
- WebSocket vs HTTP โ how does the upgrade work?
- WebSocket vs SSE vs polling โ when would you use each?
- What does Socket.IO add on top of WebSocket?
- What are rooms and namespaces?
- How do you authenticate socket connections?
- How do you scale Socket.IO across multiple servers? โ Redis adapter + sticky sessions.
- Messages appear twice in the React chat โ why?
- How do you make sure a user does not miss messages while offline?
โ Cluster, Worker Threads & Scaling | Index | Next: Redis Caching & Job Queues โ