WebSockets (ws & Socket.IO)
Reviewed & published by Brayan K
WebSockets keep a single connection open so the server and client can push messages to each other at any time, making them the foundation for real-time features like chat, live dashboards, and multiplayer games.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll see how WebSockets differ from request/response HTTP, write a server with the lean ws library, use Socket.IO's events, rooms, and broadcasting, and learn when a WebSocket is the right tool versus plain HTTP.
What You'll Learn in This Lesson
- How WebSockets differ from request/response HTTP
- The emit/on event model behind real-time messaging
- Writing a WebSocket server with the ws library
- Socket.IO events, rooms, and broadcasting
- Sending to everyone, a room, or everyone-but-the-sender
- When to use WebSockets vs HTTP or Server-Sent Events
1️⃣ HTTP vs WebSocket: the emit/on Model
With HTTP, the client asks and the server answers, then the exchange is over. With a WebSocket, the connection stays open and both sides communicate by emitting named events and listening for them — exactly the pattern of Node's EventEmitter, which is why we use it to model the idea.
In the snippet below, socket.on('name', handler) subscribes to an event and socket.emit('name', data) fires one. The server reacts to a client message and pushes one back — the two-way flow that defines WebSockets.
// A WebSocket is a single long-lived connection over which BOTH sides can
// send messages at any time. We model that "emit/on" style with Node's
// built-in EventEmitter — the same mental model Socket.IO uses.
const { EventEmitter } = require('node:events');
const socket = new EventEmitter();
// The server side LISTENS for named events from the client.
socket.on('chat message', (msg) => {
console.log(`Server received: ${msg}`);
// ...and pushes a message back, any time it wants.
socket.emit('reply', `echo: ${msg}`);
});
// The client side LISTENS for the server's pushes.
socket.on('reply', (msg) => console.log(`Client received: ${msg}`));
// The client EMITS an event. No request/response cycle — just a message.
socket.emit('chat message', 'Hello WebSockets');2️⃣ A Real Server with the ws Library
The ws library is the lean, standards-based choice. You create a WebSocketServer, handle the 'connection' event for each client, then listen for that client's 'message' and call socket.send(...) to push data back. The browser's built-in WebSocket object talks to it directly — no client library needed.
// The minimal "ws" library — closest to the raw WebSocket protocol.
// Install once: npm install ws
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
console.log('A client connected');
// Fired every time this client sends a message.
socket.on('message', (data) => {
console.log('Received:', data.toString());
socket.send('echo: ' + data.toString()); // push a message back
});
socket.on('close', () => console.log('Client disconnected'));
});
// On the browser side:
// const ws = new WebSocket('ws://localhost:8080');
// ws.onmessage = (e) => console.log(e.data);
// ws.onopen = () => ws.send('Hello');3️⃣ Socket.IO: Events, Rooms & Broadcasting
Socket.IO layers convenience on top of WebSockets: named events, automatic reconnection, and rooms. A room is a named group of connections. A socket join()s a room, and you broadcast with io.to(room).emit(...) (everyone in it) or socket.to(room).emit(...) (everyone except the sender).
// Socket.IO adds events, rooms, broadcasting, and auto-reconnect on top of ws.
// Install once: npm install socket.io
const { Server } = require('socket.io');
const io = new Server(3000);
io.on('connection', (socket) => {
// Put this connection into a named room.
socket.on('join', (room) => {
socket.join(room);
// Broadcast to EVERYONE in the room except the sender.
socket.to(room).emit('system', `A user joined ${room}`);
});
// Relay a chat message to the room.
socket.on('chat', ({ room, text }) => {
io.to(room).emit('chat', text); // io.to(...) includes the sender too
});
});Here's the rooms-and-broadcast logic as a runnable plain-Node model, so you can see exactly who receives a broadcast and who doesn't:
// Socket.IO groups connections into "rooms" and lets you broadcast to a room.
// Here is a tiny in-memory model of that idea, runnable in plain Node.
const rooms = new Map(); // roomName -> Set of client ids
function join(client, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(client);
console.log(`${client} joined room "${room}"`);
}
function broadcast(room, from, message) {
const members = rooms.get(room) || new Set();
for (const client of members) {
if (client === from) continue; // don't echo back to the sender
console.log(`-> ${client} got: "${message}" (from ${from})`);
}
}
join('alice', 'general');
join('bob', 'general');
join('carol', 'general');
broadcast('general', 'alice', 'hi everyone');Your turn. The ping/pong below works once you fill in the single blank marked ___. Follow the 👉 hint, then run it.
// 🎯 YOUR TURN — replace each ___ then run it.
const { EventEmitter } = require('node:events');
const socket = new EventEmitter();
socket.on('ping', () => {
console.log('Server got ping');
socket.emit(___); // 👉 reply with a 'pong' event: 'pong'
});
socket.on('pong', () => console.log('Client got pong'));
socket.emit('ping');
// ✅ Expected output:
// Server got ping
// Client got pongMini-Challenge: A Two-Client Chat
No blanks — just a brief and an outline. Model a two-person chat over one shared event bus, where each client sees the other's messages but not its own. Write it, run it, and compare with the example output.
// 🎯 MINI-CHALLENGE: a 2-client chat over a shared event bus
// 1. Make a shared EventEmitter "bus".
// 2. client(name) subscribes to 'message' events but SKIPS its own messages
// (compare the message's "from" to its own name), then logs what it sees.
// 3. Return an object with send(text) that emits { from: name, text }.
// 4. Create alice and bob, have alice send "hello bob" and bob send "hi alice".
//
// ✅ Example output:
// [bob] sees: alice: hello bob
// [alice] sees: bob: hi alice
const { EventEmitter } = require('node:events');
// your code hereCommon Errors (and the fix)
- ❌ Printing <Buffer ...> instead of text — ws delivers messages as Buffers. ✅ Call data.toString() to read them as strings.
- ❌ Browser native WebSocket can't connect to a Socket.IO server — Socket.IO uses its own protocol. ✅ Use the socket.io-client on the front end, or use ws on the server for the native API.
- ❌ Broadcasts go to everyone including the sender — io.to(room) includes the sender. ✅ Use socket.to(room) to exclude them.
- ❌ Real-time breaks across multiple server instances — each instance only knows its own connections. ✅ Add the Socket.IO Redis adapter (or sticky sessions) so rooms span all instances.
- ❌ Mixed ws:// on an https page — browsers block insecure WebSockets on secure pages. ✅ Use wss:// over TLS in production.
Pro Tips
- 💡 Pick the smallest tool that fits: native ws for a lean custom protocol, Socket.IO when you want rooms, reconnection, and fallbacks for free.
- 💡 For one-way pushes, consider Server-Sent Events: they're simpler than WebSockets when only the server needs to push.
- 💡 Scale with the Redis adapter: it lets rooms and broadcasts work across many Node instances behind a load balancer.
📋 Quick Reference — WebSocket Messaging
| Task | Syntax |
|---|---|
| Listen for an event | socket.on('name', fn) |
| Send an event | socket.emit('name', data) |
| ws: send to a client | socket.send(text) |
| Join a room | socket.join(room) |
| To everyone in a room | io.to(room).emit(...) |
| To room, not sender | socket.to(room).emit(...) |
| Secure connection | wss://host |
🎉 Lesson Complete!
- ✅ WebSockets keep one connection open for two-way, real-time messaging
- ✅ The emit/on event model mirrors Node's EventEmitter
- ✅ The ws library gives a lean, standards-based server
- ✅ Socket.IO adds rooms, broadcasting, reconnection, and fallbacks
- ✅ io.to(room) includes the sender; socket.to(room) excludes them
- ✅ Use WebSockets for frequent two-way updates; HTTP or SSE for simpler cases
Practice quiz
What is the key difference between HTTP and WebSockets?
- WebSockets keep one connection open so either side can push anytime
- HTTP is always faster
- WebSockets cannot send text
- HTTP keeps a persistent socket
Answer: WebSockets keep one connection open so either side can push anytime. A WebSocket is a persistent full-duplex channel; HTTP is request/response.
Which Node built-in models the emit/on event style used by Socket.IO?
- fs
- EventEmitter
- Buffer
- cluster
Answer: EventEmitter. EventEmitter's on()/emit() pattern mirrors how WebSocket messaging works.
With the ws library, in what form do incoming messages arrive?
- A parsed JSON object
- A number
- A Buffer (call .toString() for text)
- An HTML string
Answer: A Buffer (call .toString() for text). ws delivers messages as Buffers; use data.toString() to read text.
How does a socket join a named group in Socket.IO?
- socket.group(name)
- io.add(name)
- socket.room = name
- socket.join(room)
Answer: socket.join(room). socket.join('room') adds the connection to that room.
Which call broadcasts to everyone in a room EXCEPT the sender?
- socket.to(room).emit(...)
- io.to(room).emit(...)
- io.emit(...)
- socket.send(...)
Answer: socket.to(room).emit(...). socket.to(room) excludes the sender; io.to(room) includes everyone.
Why can't a browser's native WebSocket connect to a Socket.IO server?
- The port is blocked
- Socket.IO uses its own protocol, needing socket.io-client
- Native WebSocket is deprecated
- Socket.IO only accepts HTTP
Answer: Socket.IO uses its own protocol, needing socket.io-client. Socket.IO has its own protocol; the browser must use socket.io-client.
When is a simpler alternative than WebSockets a better fit?
- For multiplayer games
- For high-frequency two-way updates
- For occasional one-way server pushes (Server-Sent Events)
- Never, WebSockets always win
Answer: For occasional one-way server pushes (Server-Sent Events). SSE is simpler when only the server needs to push occasionally.
Which library is the lean, standards-based WebSocket choice?
- socket.io
- express
- axios
- ws
Answer: ws. ws speaks the raw WebSocket protocol the browser's WebSocket understands.
On an HTTPS page, which WebSocket scheme must you use?
- wss://
- ws://
- http://
- ftp://
Answer: wss://. Browsers block insecure ws:// on secure pages; use wss:// over TLS.
What problem appears when running Socket.IO across multiple instances?
- Messages are encrypted twice
- Each instance only knows its own connections, so rooms break
- The port changes randomly
- Clients can't reconnect at all
Answer: Each instance only knows its own connections, so rooms break. You need the Redis adapter or sticky sessions so rooms span all instances.
Continue this course
- Previous: Scaling with the cluster Module
- Next: GraphQL APIs — Query exactly what you need with schemas, resolvers, and Apollo
- Quick reference: Node.js cheat sheet
Frequently asked questions
What is the difference between HTTP and WebSockets?
HTTP is request/response: the client asks, the server answers, and the connection is effectively done. To get new data, the client must ask again (polling). WebSockets upgrade a single HTTP connection into a persistent, full-duplex channel that stays open, so either side can push a message at any moment without a new request. That makes WebSockets ideal for real-time features — chat, live dashboards, multiplayer games, notifications — where the server needs to tell the client about something the instant it happens, rather than waiting for the client to poll.
Should I use the ws library or Socket.IO?
Use ws when you want a thin, standards-compliant WebSocket server with minimal overhead and you're happy to handle reconnection, message framing, and fallbacks yourself — it speaks the raw WebSocket protocol that browsers' built-in WebSocket understands. Use Socket.IO when you want batteries included: named events, rooms, broadcasting, automatic reconnection, and a fallback to HTTP long-polling when a WebSocket can't be established (behind some proxies/firewalls). The trade-off is that Socket.IO has its own protocol, so the browser must use the socket.io-client, not the native WebSocket. For most chat-style apps, Socket.IO's features save real time; for a lean, custom protocol, ws is leaner.
What are rooms in Socket.IO?
A room is just a named group of connected sockets — a label you attach to connections so you can message many of them at once. A socket joins a room with socket.join('room42'), and you broadcast to that group with io.to('room42').emit(event, data) (everyone in the room) or socket.to('room42').emit(...) (everyone except the sender). Rooms are how you build chat channels, per-game lobbies, or per-document collaboration: instead of tracking which sockets care about which topic yourself, you let Socket.IO keep the membership sets and fan a message out to exactly the right connections.
When should I NOT use WebSockets?
WebSockets shine for frequent, low-latency, bidirectional updates, but they're overkill when you only need occasional server-to-client pushes — there, Server-Sent Events (a simpler one-way stream over HTTP) are often a better fit. They're also unnecessary for ordinary request/response data fetching, where plain HTTP/REST is simpler to cache, debug, and scale. And because a WebSocket holds an open connection per client, they add memory and connection-management overhead, and need sticky sessions or a shared adapter (like the Redis adapter) when you run multiple instances. If your data changes rarely or your interactions are naturally request/response, stick with HTTP.