Ordinary HTTP works like a letter: the client asks, the server answers, and the conversation is over. That's perfect for loading pages and calling APIs, but not for things that change constantly: chat messages, live sports scores, collaborative editing, multiplayer games, delivery tracking, or a dashboard of orders arriving. Asking the server "anything new?" every few seconds, called polling, wastes bandwidth and still feels slow.
WebSockets keep a single connection open so both sides can send messages at any moment, with very little overhead. Node.js is particularly good at this: because it's event-driven and non-blocking, one process can hold tens of thousands of open connections, each costing only a little memory while idle.
In this lesson you'll build a WebSocket server with the popular ws package, exchange JSON messages, broadcast to everyone and to rooms, keep connections healthy with heartbeats, and check who's connecting. You'll also meet Server-Sent Events, a simpler, HTTP-based option when only the server needs to push updates.
A WebSocket server
npm install wsimport { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket, req) => {
socket.send(JSON.stringify({ type: 'welcome' }));
socket.on('message', (data) => {
const msg = JSON.parse(data);
console.log('Received', msg);
});
socket.on('close', () => console.log('Client left'));
});- The connection starts as an HTTP request that's upgraded to a WebSocket, so it works through the same ports and proxies as your website.
- Each client gets its own
socket;sendsends a message and themessageevent receives one. - Messages are text or binary. Use JSON with a
typefield, such as{ "type": "chat", "text": "Hi" }, so both sides know how to handle each message. - A
WebSocketServercan share a port with Express:new WebSocketServer({ server })using thehttp.Serverreturned byapp.listen.
Broadcasting, rooms and the browser
To send a message to everyone, loop over wss.clients and send to each socket whose readyState is WebSocket.OPEN. For rooms, such as one per chat channel or document, keep a Map from room name to a Set of sockets, add sockets when they join and remove them when they close.
Browsers have WebSockets built in:
const ws = new WebSocket('wss://shop.example/live');
ws.onmessage = (event) => showOrder(JSON.parse(event.data));
ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 'orders' }));Use wss:// (secure WebSockets) in production, just as you use https://.
Reliability and Server-Sent Events
- Connections can drop silently, for example when a phone loses signal. A heartbeat sends a
pingregularly and closes sockets that don't answer with apong. Clients should reconnect automatically with a growing delay. - Check authentication when the connection opens, using a cookie or a token, and close unauthorised sockets.
- With several server instances, a message received by one server must reach clients on the others; Redis pub/sub is the usual way to share it.
Server-Sent Events (SSE) are a lighter option when only the server sends updates, such as notifications or progress bars. The server keeps an HTTP response open with Content-Type: text/event-stream and writes lines like data: {...}\n\n; browsers read them with new EventSource(url), which reconnects automatically.
Node.js Advanced Syntax
WebSocket essentials
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ server }); // share the Express server
wss.on('connection', (ws, req) => {
ws.on('message', (data) => handle(JSON.parse(data)));
ws.on('close', () => leaveRooms(ws));
ws.send(JSON.stringify({ type: 'hello' }));
});
for (const c of wss.clients) if (c.readyState === WebSocket.OPEN) c.send(msg);
// Server-Sent Events
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
res.write(`data: ${JSON.stringify(update)}\n\n`);Node.js Advanced Summary
- WebSockets keep one connection open for two-way, low-latency messages.
- The ws package's WebSocketServer emits connection; sockets emit message and close.
- Send JSON with a type field; broadcast by looping over open clients; use Maps of Sets for rooms.
- Use heartbeats to detect dropped connections and authenticate when a socket connects.
- Server-Sent Events are simpler for one-way server-to-browser updates.
Node.js Advanced Examples
Each example shows the code, the output it produces and what each part does.
An echo server with JSON messages
import { WebSocketServer, WebSocket } from 'ws';
const wss = new WebSocketServer({ port: 0 });
wss.on('connection', (socket) => {
socket.send(JSON.stringify({ type: 'welcome', text: 'Connected' }));
socket.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'ping') socket.send(JSON.stringify({ type: 'pong', at: msg.at }));
else socket.send(JSON.stringify({ type: 'echo', text: msg.text.toUpperCase() }));
});
});
wss.on('listening', () => {
const client = new WebSocket(`ws://localhost:${wss.address().port}`);
let received = 0;
client.on('open', () => {
client.send(JSON.stringify({ type: 'chat', text: 'hello there' }));
client.send(JSON.stringify({ type: 'ping', at: 1 }));
});
client.on('message', (data) => {
console.log(JSON.parse(data));
if (++received === 3) {
client.close();
wss.close();
}
});
});{ type: 'welcome', text: 'Connected' }
{ type: 'echo', text: 'HELLO THERE' }
{ type: 'pong', at: 1 }Line-by-line explanation
- Line 3Port 0 picks an unused port for the demo; a real server would use process.env.PORT.
- Line 5The connection event fires once for each client that connects.
- Lines 7 to 11Each incoming message is parsed and handled according to its type.
- Line 15The ws package also includes a client, used here to talk to the server; browsers have their own WebSocket class.
- Lines 17 to 20Messages can be sent as soon as the connection is open.
Broadcasting to rooms
import { WebSocketServer, WebSocket } from 'ws';
const rooms = new Map();
const wss = new WebSocketServer({ port: 0 });
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
socket.room = room;
}
function broadcast(room, message) {
for (const s of rooms.get(room) ?? []) {
if (s.readyState === WebSocket.OPEN) s.send(JSON.stringify(message));
}
}
wss.on('connection', (socket) => {
socket.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'join') join(socket, msg.room);
if (msg.type === 'say') broadcast(socket.room, { room: socket.room, from: msg.from, text: msg.text });
});
socket.on('close', () => rooms.get(socket.room)?.delete(socket));
});
wss.on('listening', async () => {
const url = `ws://localhost:${wss.address().port}`;
const connect = (name, room) => new Promise((resolve) => {
const ws = new WebSocket(url);
ws.on('open', () => { ws.send(JSON.stringify({ type: 'join', room })); resolve(ws); });
ws.on('message', (d) => console.log(`${name} got:`, d.toString()));
});
const ada = await connect('ada', 'kitchen');
const ben = await connect('ben', 'kitchen');
const cy = await connect('cy', 'garden');
await new Promise((r) => setTimeout(r, 50));
ada.send(JSON.stringify({ type: 'say', from: 'ada', text: 'Kettle is on' }));
await new Promise((r) => setTimeout(r, 50));
for (const ws of [ada, ben, cy]) ws.close();
wss.close();
});ada got: {"room":"kitchen","from":"ada","text":"Kettle is on"}
ben got: {"room":"kitchen","from":"ada","text":"Kettle is on"}Line-by-line explanation
- Line 3Each room name maps to a Set of the sockets in it.
- Lines 6 to 10Joining adds the socket to the room's set and remembers the room on the socket.
- Lines 12 to 16Broadcasting sends to every open socket in the room, and only that room.
- Line 24When a client disconnects, it's removed so the room doesn't keep dead sockets.
- Line 38Cy is in a different room, so only Ada and Ben receive the message.
Server-Sent Events for live updates
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
let n = 0;
const timer = setInterval(() => {
n++;
res.write(`event: progress\ndata: ${JSON.stringify({ percent: n * 25 })}\n\n`);
if (n === 4) {
clearInterval(timer);
res.end();
}
}, 20);
req.on('close', () => clearInterval(timer));
});
server.listen(0, async () => {
const r = await fetch(`http://localhost:${server.address().port}/export-progress`);
const text = await r.text();
const updates = text.trim().split('\n\n').map((block) => JSON.parse(block.split('data: ')[1]).percent);
console.log(r.headers.get('content-type'));
console.log(updates.join('% ') + '%');
server.close();
});text/event-stream
25% 50% 75% 100%Line-by-line explanation
- Line 5The text/event-stream content type tells browsers this is an SSE stream.
- Line 12Each event is an optional event name and a data line, ended by a blank line.
- Line 18If the browser disconnects, the timer is cleaned up.
- Lines 22 to 24The demo reads the stream with fetch; browsers would use new EventSource(url).
Common Node.js Advanced Mistakes
Sending objects without JSON
socket.send({ type: 'chat', text: 'Hi' }); // sends "[object Object]"socket.send(JSON.stringify({ type: 'chat', text: 'Hi' }));Trusting incoming messages
socket.on('message', (data) => {
const msg = JSON.parse(data); // crashes on invalid JSON
broadcast(msg.room, msg); // lets clients post to any room as anyone
});socket.on('message', (data) => {
const msg = ChatMessage.safeParse(safeJson(data));
if (!msg.success) return;
broadcast(socket.room, { from: socket.user.name, text: msg.data.text });
});Never removing closed sockets
wss.on('connection', (s) => everyone.add(s)); // the set grows foreverwss.on('connection', (s) => {
everyone.add(s);
s.on('close', () => everyone.delete(s));
});Node.js Advanced in Real Projects
Real-time features in practice
Chat apps, trading dashboards, live sports scores, collaborative editors like Figma and Google Docs, ride-hailing maps and multiplayer games all depend on WebSockets. Notification bells and progress bars often use Server-Sent Events. Production systems put WebSocket servers behind load balancers that support long-lived connections, share messages between instances with Redis or a message broker, and authenticate every connection.
Node.js Advanced FAQ
Does Node.js have built-in WebSocket support?
Partly. Node 22 includes a global WebSocket client, the same API browsers use, so a script can connect to a WebSocket server without installing anything. There's still no built-in WebSocket server, which is why servers use a package such as ws or a framework built on it.
What is the difference between WebSockets and HTTP polling?
With polling, the client asks the server for updates every few seconds, which wastes requests when nothing has changed and adds delay when something has. A WebSocket keeps one connection open, so either side can send a message the moment there's something to say. Polling is simpler to run and fine for slow-changing data; WebSockets suit chat, games and live collaboration.
Can a Socket.IO client connect to a plain WebSocket server?
No. Socket.IO uses WebSockets underneath but adds its own protocol for events, acknowledgements and reconnection, so its client needs a Socket.IO server and a plain WebSocket can't talk to a Socket.IO server directly. Choose one approach for both ends of the connection.
Why do my WebSocket connections drop after about a minute?
A proxy or load balancer in front of your app is probably closing connections it thinks are idle; Nginx, for example, times out a proxied connection after 60 seconds without data by default. Send a heartbeat ping more often than the timeout, raise the proxy's timeout for the WebSocket route, and make sure the proxy is configured to pass on the Upgrade and Connection headers.
How many WebSocket connections can one Node.js server handle?
Often tens of thousands, because an idle connection costs little CPU. The real limits are memory per connection, how much work each message triggers, and the operating system's limit on open files, which may need raising. Measure with a load-testing tool, and plan to share messages between several instances with something like Redis once one server isn't enough.
Node.js Advanced Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line to send a JSON message over the socket.
socket.(JSON.stringify({ type: 'hello' }));
What's the main advantage of WebSockets over repeated HTTP requests?
Which event does a ws socket emit when a message arrives?
When are Server-Sent Events a better choice than WebSockets?
Why add a heartbeat with ping and pong?
Node.js Advanced Quiz
5 questions to check what you learned.
Which URL scheme is used for secure WebSockets?
- https://
- ws+ssl://
- wss://
- sws://
How do you send a message to every connected client with ws?
- wss.sendAll()
- socket.broadcast()
- Loop over wss.clients and send to each open socket
- It isn't possible
What Content-Type does a Server-Sent Events response use?
- text/event-stream
- application/json
- text/plain
- application/sse
Why include a type field in WebSocket messages?
- It's required by the protocol
- To compress them
- For security
- So each side knows how to handle each kind of message
How do several WebSocket servers share messages so every client receives them?
- They can't
- Through a shared message channel such as Redis pub/sub
- By using one port
- By sending larger messages