Realtime with Socket.IO
Raw WebSockets give you a bare two-way pipe; Socket.IO gives you a batteries-included realtime framework on top of it. It adds automatic reconnection, fallback transports, message acknowledgements, named events, rooms, namespaces, and a clean broadcasting API — the plumbing you would otherwise hand-roll. Because Socket.IO attaches to the same http.Server that Express already runs on, you keep one port and one process while gaining a production-grade event layer. This page shows how to wire it up, emit and listen for events, organize clients with rooms and namespaces, and share your existing Express session and auth context.
Attaching Socket.IO to the Express server
app.listen() returns a Node http.Server, but to share it with Socket.IO you should create the server explicitly so both layers can reference the same instance. Socket.IO then hooks the upgrade handshake while Express continues to serve ordinary routes.
npm install express socket.io
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
app.get('/', (req, res) => res.send('HTTP route still works'));
const server = http.createServer(app);
const io = new Server(server, {
cors: { origin: 'http://localhost:5173' }, // allow your frontend origin
});
io.on('connection', (socket) => {
console.log('client connected:', socket.id);
socket.on('disconnect', (reason) => {
console.log('client left:', socket.id, reason);
});
});
server.listen(3000, () => console.log('HTTP + Socket.IO on :3000'));
Output:
HTTP + Socket.IO on :3000
client connected: pK3v9Lq2X7aB0001
client left: pK3v9Lq2X7aB0001 transport close
The browser client connects with the companion library, which handles reconnection automatically.
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.on('connect', () => console.log('connected as', socket.id));
Emitting and listening for events
Unlike raw ws, where every message is an opaque blob you must parse, Socket.IO uses named events with structured payloads. The server listens with socket.on(event, handler) and pushes with socket.emit(event, payload). Payloads are serialized for you, so objects, arrays, and binary Buffers all work.
io.on('connection', (socket) => {
socket.emit('welcome', { id: socket.id, ts: Date.now() });
socket.on('chat:message', (msg) => {
// Broadcast to everyone EXCEPT the sender
socket.broadcast.emit('chat:message', { text: msg.text, from: socket.id });
});
});
The table below summarizes the emit targets you will reach for most often.
| Call | Reaches | Typical use |
|---|---|---|
socket.emit(...) | Only this client | Replies, private data |
socket.broadcast.emit(...) | Everyone except sender | ”User is typing”, chat echoes |
io.emit(...) | Every connected client | Global announcements |
io.to(room).emit(...) | All clients in a room | Channel/group messages |
socket.to(room).emit(...) | Room members except sender | Room chat from this client |
Acknowledgements
Pass a callback as the last argument to emit and the receiver can invoke it to confirm receipt — a clean request/response pattern over a realtime channel.
socket.on('order:create', (data, ack) => {
const orderId = saveOrder(data);
ack({ ok: true, orderId }); // resolves the client's emitWithAck()
});
// client
const res = await socket.emitWithAck('order:create', { item: 'sku-42' });
console.log(res); // { ok: true, orderId: '...' }
Rooms and namespaces
A room is an arbitrary, server-side label you can join sockets into. A socket can belong to many rooms, and you broadcast to a room by name. Rooms are perfect for chat channels, document sessions, or per-user notification streams.
io.on('connection', (socket) => {
socket.on('room:join', (room) => {
socket.join(room);
io.to(room).emit('system', `${socket.id} joined ${room}`);
});
socket.on('room:message', ({ room, text }) => {
socket.to(room).emit('chat:message', { text, from: socket.id });
});
});
A namespace is a higher-level split of the communication channel under a path like /admin or /chat. Namespaces have their own connection handlers, middleware, and rooms — use them to isolate distinct features or access levels.
const adminNs = io.of('/admin');
adminNs.on('connection', (socket) => {
socket.emit('metrics', collectMetrics());
});
// client connects to a specific namespace
const admin = io('http://localhost:3000/admin');
Tip: Every socket automatically joins a private room named after its own
socket.id. That is whyio.to(socket.id).emit(...)delivers to exactly one client — handy for targeted notifications without tracking sockets yourself.
Sharing Express session and auth context
WebSocket handshakes bypass app.use middleware, so Socket.IO does not automatically see your Express session or auth. Bridge them with Socket.IO middleware — functions registered via io.use(...) that run once per connecting socket. You can reuse the same express-session instance for both HTTP and sockets.
const session = require('express-session');
const sessionMiddleware = session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
});
app.use(sessionMiddleware);
// Wrap the Express middleware so Socket.IO can run it on the handshake
io.engine.use(sessionMiddleware);
io.use((socket, next) => {
const { user } = socket.request.session || {};
if (!user) return next(new Error('unauthorized'));
socket.data.user = user; // attach for later handlers
next();
});
io.on('connection', (socket) => {
socket.emit('welcome', { name: socket.data.user.name });
});
For token-based auth, clients send credentials in the handshake auth payload, which you validate in the same middleware.
// client
const socket = io('http://localhost:3000', { auth: { token: jwt } });
// server
io.use((socket, next) => {
try {
socket.data.user = verifyJwt(socket.handshake.auth.token);
next();
} catch {
next(new Error('invalid token'));
}
});
Warning: Never trust
socket.idas an identity — it is a transient connection id, not a user id. Authenticate inio.useand store the verified user onsocket.dataso every event handler reads a server-validated identity.
Best Practices
- Share the single
http.Serverwith Express so realtime and HTTP run on one port and one deploy unit. - Use clear, namespaced event names like
chat:messageandorder:createinstead of genericmessageevents. - Authenticate in
io.usemiddleware and keep the verified user onsocket.data— handshakes skip Express middleware. - Prefer rooms over manually tracking socket arrays; let Socket.IO manage membership and fan-out.
- Use acknowledgements (
emitWithAck) when a client needs confirmation that the server handled an event. - When scaling to multiple Node processes, add the
@socket.io/redis-adapterso rooms and broadcasts span instances. - Behind a proxy, enable
Upgrade/Connectionpassthrough and sticky sessions so the handshake and polling fallback reach the same worker.