WebSocket & Real-Time Features Guide: Chat, notifications, live updates
Open any decent product today and real-time features are everywhere: live chat for instant conversations, real-time notifications in admin panels, collaborative document editing, dashboards that refresh on their own. The core technology behind all of this is WebSocket. This guide walks through choosing the right approach, provides a runnable front-end/back-end example, and covers the production pitfalls people actually hit — so your code doesn't work in dev and die in production.
WebSocket vs SSE vs Polling
| Feature | Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Client→Server | Server→Client | Bidirectional |
| Real-time | Varies | Instant | Instant |
| Overhead | High | Low | Low |
| Best for | Simple refresh | Push notifications | Interactive apps |
A quick rule of thumb: if the server just needs to push new data to the client (new comments, order status, stock prices), SSE is enough and takes about half the code of WebSocket. Only go with WebSocket when the client also needs to send messages at any time and the server responds in real time — chat, collaborative editing, games. Polling is fine for pages that change rarely and can tolerate a refresh every few minutes. When choosing, treat "is it bidirectional" as the first criterion and you'll avoid most detours.
WebSocket Server (Node.js + ws)
npm init -y
npm install ws
// server.js
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
const clients = new Set();
server.on('connection', (ws) => {
clients.add(ws);
console.log('Client connected. Total:', clients.size);
// Receive a message and broadcast it to every client
ws.on('message', (message) => {
const data = JSON.parse(message);
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'message',
user: data.user,
content: data.content,
time: new Date().toISOString()
}));
}
});
});
// Handle disconnect
ws.on('close', () => {
clients.delete(ws);
console.log('Client disconnected. Total:', clients.size);
});
});
console.log('WebSocket server running on port 8080');
This is a minimal broadcast chat server: every connection joins a Set, and incoming messages are forwarded to all online clients. In production you'd add user authentication, message persistence, heartbeat keep-alive, and horizontal scaling across multiple processes. For a first deployment, isolate traffic by room or channel so messages don't all land in one broadcast.
WebSocket Client
// Connect to the WebSocket server
const ws = new WebSocket('wss://example.com/ws');
// Connection opened
ws.addEventListener('open', () => {
console.log('Connected');
// Send a message
ws.send(JSON.stringify({
user: 'Alice',
content: 'Hello everyone!'
}));
});
// Receive a message
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
displayMessage(data.user, data.content);
});
// Connection closed
ws.addEventListener('close', () => {
console.log('Disconnected');
// Simple reconnect: reload the page after 3 seconds
setTimeout(() => {
window.location.reload();
}, 3000);
});
// Error handling
ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});
Three client-side details are easy to miss: handle close and implement reconnection (mobile network switches kill connections all the time); separate message types by a type field instead of mixing control and business messages; and check readyState before send, since calling it while not OPEN throws.
SSE (Server-Sent Events) Option
If you only need server-side push (no client-to-server messages), SSE is the simpler choice:
// Server (Node.js)
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// Push the current time once per second
setInterval(() => {
res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
}, 1000);
});
// Client
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Server time:', data.time);
};
SSE rides on plain HTTP, needs no extra protocol or port, and is easy to put behind Nginx; it even reconnects automatically after network hiccups. The downside: it's one-way, so the client sends data through ordinary POST requests. For scenarios like "push order status changes to an admin panel", SSE is the most comfortable choice — a single EventSource on the front end does it.
Deployment Notes
- Nginx proxying WebSocket — requires the Upgrade header; the default config drops idle connections after 60 seconds, so raise
proxy_read_timeoutand enable HTTP/1.1; - Auto-reconnect — connections can drop due to network switches and proxy timeouts; design a backoff reconnect on the client;
- Connection management — track online user counts and cap connections per machine so leaked sockets don't kill the process;
- Scaling — in multi-server environments, use Redis Pub/Sub to forward messages so clients on different nodes can talk to each other.
The hard part of real-time features is never "can it connect" — it's "does it heal when disconnected, and does it hold up when crowded". Get the selection and a basic example working first, then add heartbeats, auth, and scaling step by step.
Reference: MDN WebSockets API https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API, MDN Server-sent events https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
16IDC Takeaway
If you only need server-side push (new comments, order status updates), SSE is simpler and has better compatibility — a single EventSource on the front end does it. If you need bidirectional interaction (live chat, collaborative editing), WebSocket is the only choice. Most sites don't actually need site-wide real-time: make the few critical scenarios real-time and leave the rest on regular requests — that's the best cost-to-benefit ratio.