WebSockets & Real-time Apps
In a normal HTTP request the client asks and the server answers, then the conversation is over. That is a problem for live features: a chat message, a cricket score update or a delivery rider's location must reach the user the moment it happens, without the user asking again and again.
WebSockets solve this. A WebSocket is a connection that stays open, and both sides can send messages at any time. In this lesson you will build a small group chat with the ws package on top of an Express server, send JSON messages to every connected user, and connect from both the browser and Node.js.
How a WebSocket connection works
- The client sends a normal HTTP request with a header asking to upgrade the connection to the WebSocket protocol, at an address starting with
ws://(orwss://for the encrypted version, likehttps://). - The server agrees, and from then on the same TCP connection carries WebSocket messages in both directions.
- Either side can close the connection.
| Approach | How it works | Good for |
|---|---|---|
| Polling | Client asks "anything new?" every few seconds | Simple dashboards; wasteful |
| Server-Sent Events (SSE) | One long HTTP response; only the server sends | Live scores, notifications, progress bars |
| WebSockets | Open two-way connection | Chat, multiplayer games, live collaboration |
Browsers have had a built-in WebSocket class for many years. Node.js also has a global WebSocket client (stable since Node.js 22), but no built-in WebSocket server, so servers use a package. ws is the most widely used one: small, fast and well tested.
npm install ws expressA chat server
The WebSocket server shares one HTTP server with Express, so the web page and the chat use the same port:
// server.js
import http from "node:http";
import express from "express";
import { WebSocketServer, WebSocket } from "ws";
const app = express();
app.use(express.static("public"));
const server = http.createServer(app); // one server for HTTP and WebSocket
const wss = new WebSocketServer({ server, path: "/chat" });
function broadcast(data) {
const text = JSON.stringify(data);
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) client.send(text);
}
}
wss.on("connection", (socket, req) => {
const name = new URL(req.url, "http://localhost").searchParams.get("name") ?? "Guest";
console.log(`${name} connected (${wss.clients.size} online)`);
broadcast({ type: "info", text: `${name} joined the chat` });
socket.on("message", (raw) => {
let message;
try {
message = JSON.parse(raw.toString());
} catch {
return socket.send(JSON.stringify({ type: "error", text: "Send JSON" }));
}
const text = String(message.text ?? "").slice(0, 500);
if (text) broadcast({ type: "chat", from: name, text });
});
socket.on("close", () => {
console.log(`${name} left`);
broadcast({ type: "info", text: `${name} left the chat` });
});
socket.on("error", (err) => console.error(`Socket error: ${err.message}`));
});
server.listen(3000, () => console.log("Chat server on http://localhost:3000"));How it works:
http.createServer(app)creates the HTTP server yourself instead of callingapp.listen(), so you can hand the same server toWebSocketServer. Requests to/chatthat ask for an upgrade become WebSocket connections; everything else goes to Express.- The
"connection"event fires once per new client with asocketfor that client and the original HTTP requestreq, from which the code reads the user's name. wss.clientsis aSetof every connected socket. Broadcasting means looping over it and sending to each one that is stillOPEN.- Messages arrive as Buffers in the
"message"event. Agreeing on JSON with atypefield keeps the protocol simple to extend. The server never trusts the content: it catches bad JSON, convertstextto a string and cuts it to 500 characters. - Every socket gets an
"error"listener. As you learned in Events & EventEmitter, an unhandled"error"event would crash the whole server.
Connecting from the browser
Put this page in public/index.html. It uses the browser's built-in WebSocket class:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Class Chat</title>
</head>
<body>
<h1>Class Chat</h1>
<ul id="messages"></ul>
<form id="chat-form">
<input id="text" autocomplete="off" required>
<button>Send</button>
</form>
<script>
const name = prompt("Your name?") || "Guest";
const protocol = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${location.host}/chat?name=${encodeURIComponent(name)}`);
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
const li = document.createElement("li");
li.textContent = msg.type === "chat" ? `${msg.from}: ${msg.text}` : msg.text;
document.querySelector("#messages").append(li);
});
document.querySelector("#chat-form").addEventListener("submit", (event) => {
event.preventDefault();
const input = document.querySelector("#text");
ws.send(JSON.stringify({ text: input.value }));
input.value = "";
});
</script>
</body>
</html>Open http://localhost:3000 in two browser windows and chat between them. The page uses textContent, never innerHTML, to show messages, so a user who types <script> cannot run code in other people's browsers.
Connecting from Node.js
Node.js has the same WebSocket class as the browser, which is handy for tests and bots. This script plays two users:
// client.js
import { setTimeout as sleep } from "node:timers/promises";
function join(name) {
const ws = new WebSocket(`ws://localhost:3000/chat?name=${encodeURIComponent(name)}`);
ws.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
console.log(`[${name}'s screen] ${msg.type === "chat" ? `${msg.from}: ${msg.text}` : msg.text}`);
});
return new Promise((resolve) => ws.addEventListener("open", () => resolve(ws)));
}
const isha = await join("Isha");
await sleep(100);
const farhan = await join("Farhan");
await sleep(100);
isha.send(JSON.stringify({ text: "Who is coming for the match on Sunday?" }));
await sleep(100);
farhan.send(JSON.stringify({ text: "Me! I will bring the bat." }));
await sleep(100);
farhan.close();
await sleep(200);
isha.close();[Isha's screen] Isha joined the chat [Farhan's screen] Farhan joined the chat [Isha's screen] Farhan joined the chat [Isha's screen] Isha: Who is coming for the match on Sunday? [Farhan's screen] Isha: Who is coming for the match on Sunday? [Isha's screen] Farhan: Me! I will bring the bat. [Farhan's screen] Farhan: Me! I will bring the bat. [Isha's screen] Farhan left the chat
Meanwhile the server terminal shows:
Chat server on http://localhost:3000 Isha connected (1 online) Farhan connected (2 online) Farhan left Isha left
Advanced: production concerns
- Authentication. Check who the user is during the upgrade: read the session cookie or a token from the request, and close the connection if it is invalid. A name in the URL, as in this demo, can be faked by anyone.
- Dead connections. Mobile users lose signal without closing properly. Send a
ping()from the server every 30 seconds and close sockets that did not answer with a"pong"event since the last check. - Message size. Set
maxPayloadinnew WebSocketServer({ ..., maxPayload: 64 * 1024 })so nobody can send huge messages. - Several server copies.
wss.clientsonly knows the users on this process. If you run several copies behind a load balancer, pass messages between them with something like Redis publish/subscribe. - Reverse proxies such as Nginx must be told to forward the
UpgradeandConnectionheaders (see Deployment).
Common mistakes
- Calling
app.listen()and creating the WebSocket server separately on another port. Share onehttp.Server, as shown. - Sending objects directly.
send()takes strings or binary data; useJSON.stringify()andJSON.parse(). - No
"error"listener on sockets. One broken connection can crash the server. - Sending to closed sockets. Check
readyState === WebSocket.OPENbefore sending. - Showing messages with
innerHTML. That allows script injection between users; usetextContent.
Key points
- WebSockets keep one connection open so the server and client can both send messages at any time.
- Use the
wspackage for the server; browsers and Node.js 22+ have a built-inWebSocketclient. - Attach
WebSocketServerto the samehttp.Serveras Express with{ server, path }. - Handle
"connection","message","close"and"error"; broadcast by looping overwss.clients. - Use JSON messages with a
typefield, validate them, and authenticate users during the upgrade.
Exercise
Add a "typing" feature to the chat. When a client sends { "type": "typing" }, the server should send { "type": "typing", "from": name } to every other connected client (not back to the sender). Change the message handler to support both chat messages and typing notices.
Show solution
socket.on("message", (raw) => {
let message;
try {
message = JSON.parse(raw.toString());
} catch {
return socket.send(JSON.stringify({ type: "error", text: "Send JSON" }));
}
if (message.type === "typing") {
const notice = JSON.stringify({ type: "typing", from: name });
for (const client of wss.clients) {
if (client !== socket && client.readyState === WebSocket.OPEN) client.send(notice);
}
return;
}
const text = String(message.text ?? "").slice(0, 500);
if (text) broadcast({ type: "chat", from: name, text });
});[Farhan's screen] {"type":"typing","from":"Isha"}Comparing client !== socket skips the sender. When Isha starts typing, only Farhan's screen receives the notice, which the page could show as "Isha is typing...".
Quick Quiz
1. What is the main difference between HTTP requests and WebSockets?
HTTP is request-response; a WebSocket is a long-lived two-way connection.
2. Which URL scheme is used for an encrypted WebSocket connection?
wss:// is WebSocket over TLS, just as https:// is HTTP over TLS.
3. How does the chat server send a message to every connected user?
wss.clients holds every connected socket, so broadcasting is a loop.
4. Why must each socket have an "error" listener?
Sockets are event emitters, and an "error" event with no listener is thrown.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
