DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

WebSockets: Reconnection, Binary Messages, Heartbeats, and Firefox DevTools Debugging

WebSockets provide full-duplex communication between browser and server — the server can push data to the client at any time, without the client polling. Firefox has excellent WebSocket support, and DevTools has a dedicated WebSocket message inspector. This guide covers the API, patterns, and debugging.

The WebSocket API

// Connect
const ws = new WebSocket('wss://api.example.com/ws');
// wss:// = WebSocket Secure (TLS); ws:// = unencrypted

// Connection events
ws.onopen = () => {
  console.log('Connected');
  ws.send('Hello, server!');
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = (event) => {
  console.log('Closed:', event.code, event.reason);
  // Reconnect if unexpected
  if (event.code !== 1000) {
    reconnect();
  }
};

// Send
ws.send('text message');
ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
ws.send(arrayBuffer); // Binary
ws.send(blob);        // Blob

// Close
ws.close(1000, 'Normal closure');
// Codes: 1000=normal, 1001=going away, 1002=protocol error, 1003=unsupported data
Enter fullscreen mode Exit fullscreen mode

WebSocket State

ws.readyState  // 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED

// Constants
WebSocket.CONNECTING // 0
WebSocket.OPEN       // 1
WebSocket.CLOSING    // 2
WebSocket.CLOSED     // 3
Enter fullscreen mode Exit fullscreen mode

Message Protocol Design

A common pattern: message envelope with type field:

// Client sends
ws.send(JSON.stringify({
  type: 'subscribe',
  channel: 'stock-prices',
  symbols: ['AAPL', 'GOOGL'],
}));

// Server sends
// { type: 'price-update', symbol: 'AAPL', price: 189.45, timestamp: 1729080000 }
// { type: 'error', message: 'Symbol not found', symbol: 'XXXX' }
// { type: 'ping' }

// Dispatch by type
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  switch (msg.type) {
    case 'price-update':
      updatePriceDisplay(msg);
      break;
    case 'error':
      showError(msg.message);
      break;
    case 'ping':
      ws.send(JSON.stringify({ type: 'pong' }));
      break;
  }
};
Enter fullscreen mode Exit fullscreen mode

Reconnection with Exponential Backoff

WebSocket connections drop. Reconnect automatically:

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.options = { maxDelay: 30000, factor: 1.5, ...options };
    this.delay = 1000;
    this.listeners = {};
    this.connect();
  }

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

    this.ws.onopen = () => {
      this.delay = 1000; // Reset on success
      this.emit('open');
    };

    this.ws.onmessage = (e) => this.emit('message', e);
    this.ws.onerror = (e) => this.emit('error', e);

    this.ws.onclose = (e) => {
      this.emit('close', e);
      if (e.code !== 1000) {
        setTimeout(() => this.connect(), this.delay);
        this.delay = Math.min(this.delay * this.options.factor, this.options.maxDelay);
      }
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    }
  }

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

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

  close() {
    this.ws.close(1000, 'Client closed');
  }
}

const ws = new ReconnectingWebSocket('wss://api.example.com/ws');
ws.on('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data);
});
ws.on('open', () => ws.send({ type: 'subscribe', channel: 'updates' }));
Enter fullscreen mode Exit fullscreen mode

Binary Messages

WebSockets support binary data efficiently:

// Receive binary
ws.binaryType = 'arraybuffer'; // or 'blob'

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const type = view.getUint8(0);      // First byte: message type
    const length = view.getUint16(1);   // Bytes 1-2: payload length
    // Process binary payload...
  } else {
    // Text message
    const data = JSON.parse(event.data);
  }
};

// Send binary
const buffer = new ArrayBuffer(5);
const view = new DataView(buffer);
view.setUint8(0, 1);     // Type: 1
view.setUint16(1, 100);  // Value: 100
ws.send(buffer);
Enter fullscreen mode Exit fullscreen mode

Binary is useful for: game state synchronization, audio/video streaming, high-frequency sensor data.

WebSocket Heartbeat

Keep the connection alive and detect dropped connections:

class HeartbeatWebSocket {
  constructor(url) {
    this.url = url;
    this.heartbeatInterval = 30000;
    this.pongTimeout = 5000;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => this.startHeartbeat();
    this.ws.onmessage = (e) => {
      if (e.data === 'pong') this.receivedPong = true;
      else this.onMessage(e);
    };
    this.ws.onclose = () => this.stopHeartbeat();
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.receivedPong = false;
      this.ws.send('ping');

      setTimeout(() => {
        if (!this.receivedPong) {
          this.ws.close(); // Connection is dead
          this.connect();  // Reconnect
        }
      }, this.pongTimeout);
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
  }
}
Enter fullscreen mode Exit fullscreen mode

Debugging WebSockets in Firefox

Firefox DevTools has first-class WebSocket support:

  1. Open DevTools → Network tab
  2. Filter to WS (WebSocket connections)
  3. Click a WebSocket connection
  4. Messages sub-tab shows every message with:
    • Direction (▲ sent, ▼ received)
    • Timestamp
    • Data (expandable for JSON)
    • Size
  5. Click any message to inspect the full payload

You can see the initial HTTP upgrade handshake in the Headers sub-tab.

WebSockets vs. Server-Sent Events vs. Polling

WebSocket Server-Sent Events Long Polling
Direction Bidirectional Server → Client only Server → Client
Protocol ws:// / wss:// HTTP HTTP
Complexity Medium Low Low
Reconnect Manual Automatic Automatic
Firefox support Full Full Full
Use when Chat, games, real-time collaboration Live feeds, notifications Simple updates

Use SSE for one-way server-to-client data (news feeds, stock tickers, progress updates). Use WebSocket for bidirectional communication.

Top comments (1)

Collapse
 
m_montazeri profile image
Mohammad Montazeri •

One shutdown edge case in ReconnectingWebSocket: after an abnormal close has queued the retry, calling close() during the backoff does not cancel that timer, so the wrapper can reconnect after the caller has asked it to stop. I would keep a terminal closed flag, save the timeout ID, clear it in close(), and guard both scheduling and connect(). A useful regression case is: disconnect, call close() before the backoff expires, then verify that no new socket is created. MDN documents timer cancellation.

AI-assisted reply; I work on Lisar Connect.