WebSockets in the Browser

· 4 min read · Updated March 16, 2026 · intermediate
websockets real-time communication browser network

WebSockets provide a persistent, full-duplex connection between your browser and a server. Unlike traditional HTTP requests where the client always initiates communication, WebSockets allow both sides to send messages at any time—making them ideal for real-time applications like chat, live dashboards, and collaborative tools.

Why WebSockets?

HTTP is inherently request-response based. To get updates from a server, your client must repeatedly poll for changes:

// Polling approach - inefficient
async function checkForUpdates() {
  const response = await fetch('/api/status');
  const data = await response.json();
  updateUI(data);
}

setInterval(checkForUpdates, 2000); // Check every 2 seconds

This wastes bandwidth and introduces latency. WebSockets solve this:

// WebSocket - server pushes updates instantly
const socket = new WebSocket('wss://example.com/ws');

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateUI(data); // Immediate update
};

WebSocket vs HTTP Comparison

AspectHTTP PollingWebSockets
Latency0-2 seconds delayInstant
OverheadNew connection each requestSingle persistent connection
Server loadHigh (many requests)Low (one connection)
Bi-directionalNo (client initiates)Yes (both can send)
Browser supportAll browsersAll modern browsers

Creating a WebSocket Connection

The WebSocket API is straightforward:

// Connect to a WebSocket server
const socket = new WebSocket('wss://example.com/ws');

// Connection opened
socket.addEventListener('open', (event) => {
  console.log('Connected to WebSocket server');
  socket.send(JSON.stringify({ type: 'hello' }));
});

// Receive messages
socket.addEventListener('message', (event) => {
  console.log('Message from server:', event.data);
});

// Handle errors
socket.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

// Connection closed
socket.addEventListener('close', (event) => {
  console.log('Disconnected:', event.code, event.reason);
});

Notice the URL scheme: ws:// for unencrypted or wss:// for encrypted connections. Always use wss:// in production—it encrypts your data and works with modern browsers without warnings.

Sending and Receiving Data

WebSockets send raw text or binary data. JSON is the common choice:

// Send a JSON message
function sendMessage(type, payload) {
  socket.send(JSON.stringify({ type, payload, timestamp: Date.now() }));
}

// Different message types
sendMessage('chat', { text: 'Hello, world!' });
sendMessage('cursor', { x: 100, y: 200 });
sendMessage('ping', {});

On the receiving end, parse the data:

socket.onmessage = (event) => {
  try {
    const message = JSON.parse(event.data);
    
    switch (message.type) {
      case 'chat':
        displayChatMessage(message.payload);
        break;
      case 'update':
        handleStateUpdate(message.payload);
        break;
      case 'pong':
        console.log('Server acknowledged our ping');
        break;
    }
  } catch (e) {
    // Handle non-JSON messages
    console.log('Raw message:', event.data);
  }
};

Handling Connection State

The WebSocket has a readyState property that tells you its current state:

console.log(socket.readyState);

// Values:
WebSocket.CONNECTING // 0 - Connection not yet established
WebSocket.OPEN      // 1 - Connection established, ready to communicate
WebSocket.CLOSING   // 2 - Connection closing
WebSocket.CLOSED    // 3 - Connection closed

This helps you avoid sending messages when the connection isn’t ready:

function safeSend(data) {
  if (socket.readyState === WebSocket.OPEN) {
    socket.send(data);
  } else {
    // Queue message or retry later
    pendingMessages.push(data);
  }
}

Automatic Reconnection

Network interruptions happen. Build reconnection logic into your code:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.connect();
  }

  connect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.onclose = (event) => {
      console.log('Connection lost, reconnecting...');
      // Exponential backoff: 1s, 2s, 4s, 8s...
      setTimeout(() => this.connect(), this.retryDelay || 1000);
      this.retryDelay = Math.min((this.retryDelay || 1000) * 2, 30000);
    };
    
    this.socket.onopen = () => {
      console.log('Reconnected');
      this.retryDelay = 1000; // Reset backoff
      this.flushPending();
    };
  }

  send(data) {
    if (this.socket.readyState === WebSocket.OPEN) {
      this.socket.send(data);
    } else {
      this.pending = this.pending || [];
      this.pending.push(data);
    }
  }

  flushPending() {
    if (this.pending) {
      this.pending.forEach(msg => this.socket.send(msg));
      this.pending = [];
    }
  }
}

const client = new WebSocketClient('wss://example.com/ws');

This client automatically reconnects with exponential backoff, preventing server overload during outages.

Real-World Example: Chat Application

Here’s a practical chat implementation:

class ChatClient {
  constructor(serverUrl) {
    this.serverUrl = serverUrl;
    this.connect();
  }

  connect() {
    this.socket = new WebSocket(this.serverUrl);
    
    this.socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };
    
    this.socket.onopen = () => {
      this.sendJoin();
    };
  }

  sendJoin() {
    this.send({ type: 'join', username: this.username });
  }

  sendMessage(text) {
    this.send({
      type: 'message',
      text: text,
      timestamp: Date.now()
    });
  }

  send(type, payload) {
    this.socket.send(JSON.stringify({ type, ...payload }));
  }

  handleMessage(message) {
    switch (message.type) {
      case 'message':
        this.displayMessage(message);
        break;
      case 'users':
        this.updateUserList(message.users);
        break;
      case 'error':
        this.showError(message.text);
        break;
    }
  }

  displayMessage(msg) {
    const chat = document.getElementById('chat');
    const div = document.createElement('div');
    div.textContent = `${msg.username}: ${msg.text}`;
    chat.appendChild(div);
  }
}

// Usage
const chat = new ChatClient('wss://chat.example.com');

// Send a message
chat.sendMessage('Hello, everyone!');

Secure WebSocket Considerations

When deploying WebSockets in production:

  • Always use wss:// — encrypts the connection
  • Validate origin — check the Origin header on the server to prevent cross-site attacks
  • Authenticate during handshake — pass tokens as query parameters or headers during connection:
    const socket = new WebSocket('wss://api.example.com/ws?token=YOUR_JWT');
  • Handle sensitive data carefully — WebSocket connections don’t automatically include cookies; use explicit authentication

Browser Compatibility

WebSockets are supported in all modern browsers:

  • Chrome 4+
  • Firefox 11+
  • Safari 4+
  • Edge 12+
  • IE 10+ (with limitations)

For older browsers, libraries like socket.io provide fallbacks, though they add overhead.

Summary

WebSockets enable real-time, bidirectional communication between browser and server. Key points:

  1. Create connections with new WebSocket(url) using wss:// for production
  2. Send messages with .send() — use JSON for structured data
  3. Handle state — check readyState before sending
  4. Implement reconnection — automatic retry with backoff handles network issues
  5. Always authenticate — validate users during the WebSocket handshake

In the next tutorial, you’ll learn about IndexedDB for storing data client-side—another powerful browser API for building offline-capable applications.