Course:Node.js & Express/
Lesson

Every time a chat message appears without you hitting refresh, a stock price ticks up in real time, or a game renders another player's move instantly, that's WebSockets at work. HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. alone can't do this cleanly. WebSockets were invented precisely for this kind of live, two-way communication.

The problem with HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. for real-time

HTTP follows a strict rule: the client always speaks first. You send a request, the server replies, and the connection closes. This works perfectly for loading a webpage or fetching a list of products. But what if the server has new data and you haven't asked for it yet?

The simplest workaround is called pollingWhat is polling?Repeatedly asking a server at regular intervals if anything has changed, which works but wastes resources when nothing is new.: your client repeatedly asks "anything new?" on a timer:

Client: Any news?  →  Server: No.
Client: Any news?  →  Server: No.
Client: Any news?  →  Server: No.
Client: Any news?  →  Server: Yes! Here's a new message.

This gets the job done, but it's brutally inefficient. Every "any news?" is a full HTTP request complete with headers, cookies, and authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token. tokens, even when the answer is always "no". Scale that to thousands of users checking every two seconds and you have a server spending most of its time saying nothing.

Good to know
A smarter variant called long polling has the server hold the connection open until it has something to say, then reply. This cuts wasted requests but you're still re-opening HTTP connections constantly. It's a patch, not a solution.
02

How WebSockets solve this

A WebSocket connection starts as a regular HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. request, called the handshakeWhat is handshake?The initial exchange between a client and server that establishes a connection and agrees on communication rules before data starts flowing., and then upgrades to a persistent, bidirectional channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue).. Once that channel is open, both client and server can send messages whenever they want. No asking permission, no new connection, no repeated headers.

Think of the difference between mailing letters and having a phone call. HTTP is letters: send one, wait for a reply, send another. WebSockets are a phone call: once you're connected, either side can speak at any moment.

Client → Server: "I'd like to upgrade to WebSocket."
Server → Client: "Done. Connection established."

[connection stays open indefinitely]

Server → Client: "New message from Alice!"
Client → Server: "Here's my reply to Alice."
Server → Client: "Bob just joined the room."

The WebSocket handshake

The upgrade starts as a regular HTTP request with a special Upgrade header:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

The server responds with 101 Switching Protocols. From that moment, the connection is no longer HTTP, it's WebSocket. This handshake happens exactly once per connection.

HTTP vs WebSocket at a glance

FeatureHTTPWebSocket
Who initiates messages?Client onlyEither side
New connection per message?YesNo, one persistent connection
Headers on every message?YesNo
Server can push data?NoYes
Best forAPIs, page loadsChat, live dashboards, multiplayer games
03

The browser WebSocket APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.

No library required, every modern browser has WebSocket support built in. The API is deliberately minimal: one constructor and four event handlers.

// Open a connection
const socket = new WebSocket('wss://my-server.com/chat');

// Fires when the connection is ready to use
socket.onopen = () => {
  console.log('Connected!');
  socket.send('Hello, server!');
};

// Fires every time the server sends data
socket.onmessage = (event) => {
  console.log('Received:', event.data);
};

// Fires on network or protocol errors
socket.onerror = (error) => {
  console.error('WebSocket error:', error);
};

// Fires when the connection closes (intentionally or not)
socket.onclose = (event) => {
  console.log('Disconnected:', event.code, event.reason);
};

Four events. That's the entire client-side API.

Sending structured data

Raw WebSocket messages are plain strings. In practice, you'll almost always serialize to JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. so you can send structured data:

// Send a typed message
socket.send(JSON.stringify({
  type: 'chat',
  room: 'general',
  text: 'Hey everyone!'
}));

// Parse incoming messages
socket.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'chat') {
    appendMessage(data.user, data.text);
  } else if (data.type === 'user-joined') {
    showNotification(`${data.user} joined the room`);
  }
};

Using a type field on every message is a common pattern. It lets you dispatch different kinds of events through a single connection.

Good to know
WebSockets also support binary data (ArrayBuffer, Blob), handy for audio streaming, images, or game state. For most web apps, JSON strings are perfectly sufficient.

Checking connection state before sending

Calling socket.send() on a closed socket throws an error. Always guard with readyState:

function safeSend(socket, data) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(JSON.stringify(data));
  } else {
    console.warn('Socket not open. Current state:', socket.readyState);
  }
}
readyState valueConstantWhat it means
0WebSocket.CONNECTINGHandshake in progress
1WebSocket.OPENReady to send and receive
2WebSocket.CLOSINGClose handshake in progress
3WebSocket.CLOSEDConnection fully closed
04

Handling disconnections

Networks are unreliable. Users lose WiFi, phones switch between mobile data and WiFi, servers restart for deployments. A real app needs to detect disconnections and reconnect automatically.

function createConnection(url) {
  const socket = new WebSocket(url);
  let attempts = 0;

  socket.onopen = () => {
    attempts = 0;
    console.log('Connected!');
  };

  socket.onclose = () => {
    if (attempts < 5) {
      attempts++;
      const delay = attempts * 2000; // 2s, 4s, 6s, 8s, 10s
      console.log(`Reconnecting in ${delay / 1000}s (attempt ${attempts}/5)...`);
      setTimeout(() => createConnection(url), delay);
    } else {
      console.error('Failed to reconnect after 5 attempts.');
    }
  };

  return socket;
}

Waiting progressively longer between retries is called exponential backoffWhat is exponential backoff?A retry strategy where each attempt waits twice as long as the previous one, giving an overloaded server progressively more time to recover.. It prevents hundreds of clients from hammering a server the instant it restarts.

Good to know
Add some random variation to the delay, delay + Math.random() * 1000, so that clients don't all retry at the exact same moment (called a thundering herd). This is especially important when a server restarts and thousands of users reconnect simultaneously.
05

When to use WebSockets

WebSockets add complexity. You're managing a persistent connection, handling reconnections, and dealing with state. Before reaching for them, ask whether you actually need two-way real-time communication.

Use WebSockets when...Stick with HTTP when...
Server needs to push updates without being askedUsers trigger every data request
Low latency matters (chat, games, collaborative editing)A few hundred milliseconds of delay is fine
Many small messages flow back and forthOccasional larger requests
Real-time dashboards, live feeds, multiplayerLogin forms, product pages, standard APIs

For cases where only the server pushes data and clients never send anything back, look at Server-Sent EventsWhat is server-sent events?A one-way, server-to-client push mechanism built on plain HTTP - simpler than WebSockets when clients never need to send data back. (SSEWhat is sse?Server-Sent Events - a one-way, server-to-client push mechanism built on plain HTTP, simpler than WebSockets for server push.) first. SSE is plain HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted., works through proxies, and is simpler to implement. WebSockets are the right choice when you genuinely need two-way, real-time communication.

javascript
// WebSocket client with automatic reconnection

class RealtimeClient {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.attempts = 0;
    this.maxAttempts = 5;
    this.listeners = {};
  }

  connect() {
    this.socket = new WebSocket(this.url);

    this.socket.onopen = () => {
      this.attempts = 0;
      console.log('✅ Connected to', this.url);
      this.emit('connect');
    };

    this.socket.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        this.emit('message', data);
        if (data.type) {
          this.emit(data.type, data);
        }
      } catch {
        this.emit('message', event.data);
      }
    };

    this.socket.onerror = (error) => {
      console.error('💥 WebSocket error:', error);
      this.emit('error', error);
    };

    this.socket.onclose = (event) => {
      console.log('❌ Disconnected:', event.code, event.reason);
      this.emit('disconnect', event);

      if (this.attempts < this.maxAttempts) {
        this.attempts++;
        const delay = this.attempts * 2000 + Math.random() * 500;
        console.log(`Reconnecting in ${(delay / 1000).toFixed(1)}s...`);
        setTimeout(() => this.connect(), delay);
      }
    };
  }

  send(type, payload = {}) {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify({ type, ...payload }));
    } else {
      console.warn('Cannot send: socket is not open');
    }
  }

  on(event, handler) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(handler);
    return this;
  }

  emit(event, data) {
    this.listeners[event]?.forEach(handler => handler(data));
  }

  disconnect() {
    this.maxAttempts = 0;
    this.socket?.close();
  }
}

// Usage
const client = new RealtimeClient('wss://my-server.com');

client
  .on('connect', () => {
    client.send('join', { room: 'general' });
  })
  .on('chat', (data) => {
    console.log(`${data.user}: ${data.text}`);
  })
  .on('user-joined', (data) => {
    console.log(`${data.user} joined`);
  })
  .on('disconnect', () => {
    console.log('Lost connection, reconnecting...');
  });

client.connect();