WebSockets Explained: Build a Chat App

HTTP can't push data to clients. WebSockets can. This guide explains the protocol, walks through building a chat app with Node.js, and covers the parts everyone skips: reconnection, heartbeats, authentication, and scaling.

Written by Projectech6 min readPublished
For B.E./B.Tech Computer Science and IT students building real-time web apps who know HTTP but need bidirectional communication Topics: WebSockets, Node.js, JavaScript, Real-time
Illustration of WebSocket bidirectional communication showing a persistent connection between browser and server with messages flowing both ways.
Illustration generated for this guide.
In this guide

HTTP is request/response: the client asks, the server answers. That works for loading pages and submitting forms. But what about a chat app, where the server needs to push new messages to the client the instant they arrive? Or a live dashboard where sensor data streams in continuously?

Polling (asking the server "anything new?" every second) works but wastes bandwidth and adds latency. WebSockets solve this properly: a single persistent connection where either side can send data at any time. This guide explains how WebSockets work and walks through building a working chat app.

What WebSockets are

WebSocket is a protocol (defined in RFC 6455) that provides full-duplex communication over a single TCP connection. The key word is full-duplex: after the connection is established, both client and server can send messages independently, without waiting for a request.

The connection starts as a normal HTTP request with an Upgrade: websocket header. If the server agrees, the connection "upgrades" from HTTP to the WebSocket protocol and stays open. Both sides can then send messages (text or binary) as frames over this persistent connection.

This is fundamentally different from HTTP:

HTTP WebSocket
Direction Client requests, server responds Either side sends anytime
Connection New connection per request (or keep-alive) One persistent connection
Server push Not possible (client must poll) Natural
Overhead per message Full HTTP headers each time ~2 byte frame header
Use case Pages, APIs, file transfer Chat, live updates, gaming, collaboration

When to use WebSockets (and when not to)

Use WebSockets when:

  • The server needs to push data to the client in real time (chat messages, notifications, live scores)
  • You need low-latency bidirectional communication (multiplayer games, collaborative editing)
  • You're sending many small messages frequently (sensor dashboards, stock tickers)

Don't use WebSockets when:

  • Simple request/response suffices (loading a page, submitting a form) — HTTP is simpler
  • You need caching, or intermediaries (CDNs, proxies) to understand the traffic
  • The data is one-directional server-to-client and infrequent — Server-Sent Events (SSE) are simpler

A common student mistake is using WebSockets for everything in a real-time project. Use WebSockets for the real-time channel and regular HTTP APIs for everything else (authentication, history loading, file uploads).

Building a chat app: the server

Using Node.js with the ws library (lightweight, no framework needed):

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

// Track connected clients
const clients = new Set();

wss.on('connection', (ws) => {
  clients.add(ws);
  console.log('Client connected. Total:', clients.size);

  // Broadcast received messages to all other clients
  ws.on('message', (data) => {
    const message = JSON.parse(data.toString());
    message.timestamp = Date.now();

    const out = JSON.stringify(message);
    clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(out);
      }
    });
  });

  ws.on('close', () => {
    clients.delete(ws);
    console.log('Client disconnected. Total:', clients.size);
  });

  ws.on('error', (err) => {
    console.error('WebSocket error:', err.message);
    clients.delete(ws);
  });
});

console.log('Chat server running on ws://localhost:8080');

The pattern is simple: on connection, add to the set. On message, broadcast to everyone else. On close or error, remove from the set. The readyState check before sending is essential — trying to send on a closed connection throws.

The client: browser JavaScript

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
  console.log('Connected to chat server');
  // Connection is ready - enable the send button
  document.getElementById('sendBtn').disabled = false;
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  addMessageToUI(msg.username, msg.text, msg.timestamp);
};

ws.onclose = () => {
  console.log('Disconnected');
  // Show reconnect UI - don't just silently fail
  showReconnectButton();
};

ws.onerror = (err) => {
  console.error('Connection error');
};

function sendMessage(username, text) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ username, text }));
  }
}

The browser's WebSocket API is built in — no library needed. The four event handlers (onopen, onmessage, onclose, onerror) cover the full lifecycle.

Handling disconnections: the part everyone skips

Networks drop. Phones switch from WiFi to cellular. Laptops sleep. A chat app that doesn't handle reconnection is a demo, not a product.

Client-side reconnection with backoff:

let ws;
let reconnectDelay = 1000;

function connect() {
  ws = new WebSocket('ws://localhost:8080');

  ws.onopen = () => {
    reconnectDelay = 1000;  // reset on successful connection
    console.log('Connected');
  };

  ws.onclose = () => {
    console.log('Reconnecting in ' + reconnectDelay + 'ms');
    setTimeout(connect, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 30000);  // exponential backoff, max 30s
  };

  ws.onmessage = (event) => {
    // ... handle message ...
  };
}

connect();

Exponential backoff (1s, 2s, 4s, 8s... capped at 30s) prevents a thousand clients from hammering your server simultaneously when it comes back online — the "thundering herd" problem.

Server-side heartbeat: if a client disconnects uncleanly (network cable pulled), the server may not notice for a long time. Implement ping/pong:

// Server: ping clients every 30 seconds, drop dead ones
setInterval(() => {
  clients.forEach((ws) => {
    if (ws.isAlive === false) {
      clients.delete(ws);
      return ws.terminate();
    }
    ws.isAlive = false;
    ws.ping();  // client auto-responds with pong
  });
}, 30000);

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
  // ... rest of connection handling
});

Authentication with WebSockets

WebSockets don't have built-in authentication. The standard approach:

  1. Client authenticates via HTTP first (login API returns a token).
  2. Client includes the token when opening the WebSocket — either as a query parameter (ws://host:8080?token=abc123) or in the initial HTTP headers.
  3. Server validates the token during the connection event and rejects unauthenticated clients immediately.
wss.on('connection', (ws, req) => {
  const token = new URL(req.url, 'http://localhost').searchParams.get('token');
  const user = verifyToken(token);  // your JWT/auth verification
  if (!user) {
    ws.close(4001, 'Unauthorized');
    return;
  }
  ws.user = user;  // attach user info for later use
  // ... normal handling ...
});

Never trust the client. Validate the token on every new connection, and don't accept user identity from message payloads — use the authenticated ws.user instead.

Scaling beyond one server

The broadcast pattern above works on a single server. When you need multiple servers (or server restarts shouldn't disconnect everyone), you need a message broker between them:

  • Redis Pub/Sub — the standard choice. Each server publishes incoming messages to Redis; each server subscribes and forwards to its connected clients. This is simple and handles most student-to-startup scale.
  • Message queue (RabbitMQ, NATS) — for more complex routing.

The architecture: Client → WebSocket Server → Redis → WebSocket Server → Client. Each server only manages its own connections; Redis handles cross-server messaging.

WebSockets vs alternatives

Technology Direction Use when
WebSockets Bidirectional Chat, games, collaboration
Server-Sent Events Server → client only Live feeds, notifications (simpler than WS)
Long polling Simulated push Legacy browser support needed
WebRTC Peer-to-peer Video/audio calls, file transfer between clients

For a good comparison of request/response API design, see the REST API design guide. If your real-time app needs user accounts, the JWT authentication guide covers the token pattern used above. More web development concepts in the Web Development branch hub.

More project guides

More in Web Development