Suggest an editImprove this articleRefine the answer for “How to integrate WebSocket with Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**WebSocket integration with Express.js** requires `http.createServer(app)` instead of `app.listen()`, then attaching Socket.IO or the `ws` library to that server. ```js const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*' } }); io.on('connection', (socket) => socket.emit('welcome', 'Hello!')); server.listen(3000); ``` **Key:** Express handles HTTP; the raw `http.Server` exposes the WebSocket `upgrade` event that makes persistent connections possible.Shown above the full answer for quick recall.Answer (EN)Image**WebSocket integration with Express.js** requires wrapping the Express app in `http.createServer(app)`, then attaching a WebSocket library to that server. Express handles HTTP routes on the same port; Socket.IO or `ws` handles persistent connections. ## Theory ### TL;DR - HTTP is a walkie-talkie: you send, wait, get a reply, connection closes. WebSocket is a phone call: both sides talk whenever, line stays open. - Express `app.listen()` alone blocks WebSocket. Always use `http.createServer(app)`. - Socket.IO adds rooms, fallback polling, and auto-reconnect. The native `ws` library is 3KB and does nothing else. - Use WebSocket when the server needs to push updates (chat, live scores, cursors). HTTP polling works fine for a dashboard refreshed every 30 seconds. - For 10k+ concurrent users, add a Redis adapter. Without it, two Node processes cannot share socket state. ### Quick setup ```js const express = require('express'); const http = require('http'); // Required for WebSocket handshake const { Server } = require('socket.io'); const app = express(); const server = http.createServer(app); // Wrap Express - not app.listen() const io = new Server(server, { cors: { origin: '*' } }); io.on('connection', (socket) => { console.log('connected:', socket.id); // fires on each new client socket.emit('welcome', 'Hello!'); // push to this client only socket.on('message', (msg) => { io.emit('message', msg); // broadcast to all clients }); socket.on('disconnect', () => { console.log('left:', socket.id); // fires on tab close or network drop }); }); app.get('/api/health', (req, res) => res.json({ ok: true })); server.listen(3000); ``` After `server.listen(3000)`, HTTP routes and WebSocket connections share port 3000. Express handles `/api/health`; Socket.IO handles `ws://localhost:3000`. ### Why Express alone cannot handle WebSocket Express is a middleware chain built on Node's `http.IncomingMessage`. It receives a request, runs it through middleware, sends a response. That is the full cycle. WebSocket starts as an HTTP request with an `Upgrade: websocket` header and expects a `101 Switching Protocols` response. After that, the connection leaves HTTP entirely and becomes raw TCP frames. The `upgrade` event lives on the raw Node `http.Server`, not on the Express app object. Express has no path to intercept it. `http.createServer(app)` gives you the actual server. Pass it to Socket.IO and it intercepts the upgrade before Express processes anything. ### When to use WebSocket - Chat, collaborative editing, live cursors: real-time bidirectional. Socket.IO on Express. - Stock tickers, push notifications, progress bars: server pushes only. WebSocket or Server-Sent Events both work here. - Dashboard refreshed every 30 seconds: HTTP polling is simpler and costs less. - Gaming lobbies or 10k+ concurrent users: WebSocket with Redis adapter. - Simple REST API with no live updates: no WebSocket needed at all. ### Socket.IO vs native ws | Feature | Socket.IO | ws | |---------|-----------|-----| | Bundle size | ~50KB | ~3KB | | Fallback to HTTP polling | Automatic | None | | Rooms / namespaces | Built-in | Manual | | Auto-reconnect | Built-in | Manual | | Browser support | Universal + legacy | 95%+ modern | | Scaling | Redis adapter available | Manual sticky sessions | | Best for | Production chat, collab apps | Custom protocols, minimal deps | Socket.IO ships 2M+ npm downloads per week. `ws` is the choice when you need to minimize bundle size or implement a custom binary protocol. ### How the handshake works When a client sends `Upgrade: websocket`, Node's libuv event loop detects it at the TCP layer. Node responds with `101 Switching Protocols`, then switches the socket from HTTP framing to WebSocket binary framing: opcode `0x1` for text, `0x2` for binary. V8 handles message parsing. Socket.IO adds its own framing layer on top for reconnection, room routing, and packet acknowledgments. After the handshake, no HTTP headers travel between client and server. That is where the minimal overhead comes from. ### Common mistakes **1. Passing the Express app directly to Socket.IO** ```js // WRONG: TypeError at runtime const io = new Server(app); ``` `express()` returns a request handler function, not an HTTP server. Fix: `const server = http.createServer(app); new Server(server)`. **2. Missing CORS config on Socket.IO** ```js // Client gets blocked in the browser - no CORS config const io = new Server(server); ``` Socket.IO's handshake is a cross-origin HTTP request before the WebSocket upgrade. Without `{ cors: { origin: 'http://localhost:3000' } }`, browsers block it. This is the single most common "two hours debugging CORS" moment in Socket.IO setups, and I've hit it more than once myself. **3. Wrong emit target in rooms** ```js socket.emit('chat', msg); // only the sender gets it io.to(roomId).emit('chat', msg); // everyone in room, sender included socket.to(roomId).emit('chat', msg); // everyone in room except sender ``` **4. No disconnect cleanup** ```js // Memory leak on hot-reload: listeners accumulate io.on('connection', () => { /* no cleanup */ }); ``` Add `socket.on('disconnect', handler)` on the server. In React, return a cleanup from `useEffect`: `return () => socket.off('chat')`. **5. Multiple Node processes without a Redis adapter** If the load balancer sends client A to process 1 and client B to process 2, `io.emit()` from process 1 only reaches clients on process 1. The Redis adapter converts every emit into a pub/sub message that all processes receive. See the Advanced example below. ### Real-world usage - Discord: sharded WebSocket gateway for 15M concurrent users. - Trello: Socket.IO on Express for live board updates. - Slack: custom `ws` implementation for real-time messaging. - ShareDB: WebSocket + Express for Google Docs-style collaborative editing (CRDT). - Pusher / Ably: managed WebSocket services that proxy through Express backends. ### Follow-up questions **Q:** Why can't you attach WebSocket directly to an Express app? **A:** Express is a middleware chain for HTTP request/response cycles. WebSocket needs the `upgrade` event on the raw Node `http.Server`. `http.createServer(app)` exposes that event. **Q:** How does Socket.IO fallback work? **A:** It detects WebSocket failure (corporate firewalls, NAT issues) and falls back to HTTP long-polling automatically. Once a stable WebSocket connection is possible, it upgrades back. **Q:** What is the difference between rooms and namespaces? **A:** Rooms are dynamic groups inside a namespace: `socket.join('room1')`. Namespaces are isolated channels with their own event scope: `io.of('/admin')`. Use rooms for chat groups, namespaces for separate features like an admin panel. **Q:** What is the difference between `socket.emit()` and `io.emit()`? **A:** `socket.emit()` sends to one client. `io.emit()` broadcasts to all connected clients. `socket.to(roomId).emit()` sends to everyone in a room except the caller. **Q:** In a multi-region setup, how do you handle WebSocket sticky sessions and avoid message loss on failover? **A:** Redis Streams instead of basic pub/sub gives you durable message history so reconnecting clients can replay missed events. Clients reconnect with session tokens so the new server restores state. For cross-region at 10k+ concurrent users, Kafka is more reliable than Redis. Sticky sessions alone fail roughly 30% of the time under load balancer restarts. A complete answer also includes TTL-based heartbeats and reconnection with exponential backoff on the client side. ## Examples ### Basic chat server with rooms ```js // server.js 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' } }); io.on('connection', (socket) => { console.log('user joined:', socket.id); socket.on('join', (room) => { socket.join(room); socket.to(room).emit('user-joined', socket.id); // notify others, not sender }); socket.on('chat', ({ room, msg }) => { io.to(room).emit('chat', msg); // everyone in room including sender }); socket.on('disconnect', () => { console.log('user left:', socket.id); }); }); server.listen(3000); ``` `socket.to(room)` excludes the sender. `io.to(room)` includes everyone. Use `socket.to()` for user messages (the sender already sees their own message locally) and `io.to()` for system events like "user joined". ### JWT authentication on Socket.IO connection ```js const jwt = require('jsonwebtoken'); // Middleware runs once before the connection event fires io.use((socket, next) => { const token = socket.handshake.auth.token; try { const user = jwt.verify(token, process.env.JWT_SECRET); socket.user = user; // attach to socket for use in event handlers next(); } catch { next(new Error('Unauthorized')); // client receives connect_error } }); io.on('connection', (socket) => { console.log('authenticated:', socket.user.id); }); ``` ```js // Client const socket = io('http://localhost:3000', { auth: { token: localStorage.getItem('jwt') } }); socket.on('connect_error', (err) => { console.log('auth failed:', err.message); // "Unauthorized" }); ``` If `next(new Error(...))` is called, the client gets `connect_error` and the socket never enters the connection handler. Good place for rate limiting and token validation. ### Redis adapter for multi-server scaling ```js // npm install @socket.io/redis-adapter ioredis 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)); io.on('connection', (socket) => { socket.on('message', (data) => { io.emit('message', data); // now reaches clients on ALL Node processes }); socket.on('disconnect', () => { // fires after ~20s timeout if no ping received console.log('user left:', socket.id); }); }); ``` Without the adapter, `io.emit()` only reaches clients on the current process. With Redis pub/sub, every emit becomes a message that all subscriber processes receive and forward to their local clients. PM2 cluster mode with this setup handles tens of thousands of concurrent connections.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.