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

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 pong

Mini-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 here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — WebSocket Messaging

TaskSyntax
Listen for an eventsocket.on('name', fn)
Send an eventsocket.emit('name', data)
ws: send to a clientsocket.send(text)
Join a roomsocket.join(room)
To everyone in a roomio.to(room).emit(...)
To room, not sendersocket.to(room).emit(...)
Secure connectionwss://host

🎉 Lesson Complete!

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

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.