WebSockets: Persistent Connections

· 6 min read · Updated March 17, 2026 · intermediate
websockets real-time javascript networking

WebSockets provide a persistent, full-duplex communication channel between client and server. Unlike traditional HTTP requests where the client always initiates communication, WebSockets allow either side to send messages at any time — making them ideal for chat applications, live dashboards, collaborative tools, and gaming.

How WebSockets Differ from HTTP

HTTP follows a request-response pattern. The client asks, the server answers, and the connection closes. Each request requires a new TCP handshake, adding latency for repeated communication:

// HTTP: New connection for each request
async function fetchUpdates() {
  const response = await fetch("/api/notifications");
  const data = await response.json();
  return data;
}

WebSockets maintain an open TCP connection after the initial handshake. Both client and server can push data without waiting for a request:

// WebSocket: Persistent connection, bidirectional
const ws = new WebSocket("wss://example.com/socket");

ws.onopen = () => {
  console.log("Connected to server");
};

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

ws.send(JSON.stringify({ type: "hello", message: "From client" }));

The wss:// protocol indicates a WebSocket connection over TLS — always use this in production.

The WebSocket Lifecycle

A WebSocket connection goes through distinct states:

  1. Connecting: The initial handshake is in progress
  2. Open: The connection is established and ready for communication
  3. Closing: The connection is being closed
  4. Closed: The connection has been closed

You can check the current state via the readyState property:

const ws = new WebSocket("wss://example.com/socket");

console.log(ws.readyState); // 0 = CONNECTING

ws.onopen = () => {
  console.log(ws.readyState); // 1 = OPEN
};

ws.onclose = () => {
  console.log(ws.readyState); // 3 = CLOSED
};

// Constants for readability
const State = {
  CONNECTING: 0,
  OPEN: 1,
  CLOSING: 2,
  CLOSED: 3
};

Client-Side Implementation

Creating a Connection

function connectWebSocket(url) {
  const ws = new WebSocket(url);
  
  ws.onopen = function() {
    console.log("WebSocket open");
  };
  
  ws.onmessage = function(event) {
    // Handle incoming message
    const message = JSON.parse(event.data);
    handleMessage(message);
  };
  
  ws.onerror = function(error) {
    console.error("WebSocket error:", error);
  };
  
  ws.onclose = function(event) {
    console.log("WebSocket closed:", event.code, event.reason);
    // Optionally attempt reconnection
  };
  
  return ws;
}

const socket = connectWebSocket("wss://api.example.com/ws");

Sending Messages

You can send text or binary data:

// Send text
socket.send(JSON.stringify({ action: "subscribe", channel: "updates" }));

// Send binary (ArrayBuffer)
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setFloat64(0, 3.14159);
socket.send(buffer);

// Send binary (Blob)
const blob = new Blob([JSON.stringify({ type: "file", data: "..." })], 
  { type: "application/json" });
socket.send(blob);

Handling Reconnection

Network issues cause WebSocket connections to close. Implement reconnection logic:

class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      if (this.onopen) this.onopen();
    };
    
    this.ws.onmessage = (event) => {
      if (this.onmessage) this.onmessage(event);
    };
    
    this.ws.onclose = (event) => {
      if (this.onclose) this.onclose(event);
      this.attemptReconnect();
    };
    
    this.ws.onerror = (error) => {
      if (this.onerror) this.onerror(error);
    };
  }

  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
      setTimeout(() => this.connect(), delay);
    }
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }
}

Server-Side Considerations

Protocol Differences

The WebSocket handshake begins as an HTTP request with an Upgrade header. The server must respond with a 101 status code to complete the upgrade:

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

Node.js WebSocket Server

Using the ws library:

const { WebSocketServer } = require("ws");

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", (ws, req) => {
  const clientIp = req.socket.remoteAddress;
  console.log("Client connected:", clientIp);

  ws.on("message", (message) => {
    // Parse and handle incoming message
    const data = JSON.parse(message);
    
    if (data.type === "ping") {
      ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
    }
  });

  ws.on("close", () => {
    console.log("Client disconnected");
  });

  // Send periodic updates
  const interval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ 
        type: "update", 
        time: new Date().toISOString() 
      }));
    }
  }, 5000);

  ws.on("close", () => clearInterval(interval));
});

Broadcasting to All Clients

wss.on("connection", (ws) => {
  ws.on("message", (message) => {
    // Broadcast to all connected clients
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
});

Security Considerations

Validating Origins

Always validate the origin header on the server to prevent cross-site WebSocket hijacking:

const ALLOWED_ORIGINS = ["https://app.example.com", "https://admin.example.com"];

wss.on("connection", (ws, req) => {
  const origin = req.headers.origin;
  if (!ALLOWED_ORIGINS.includes(origin)) {
    ws.close(1008, "Origin not allowed");
    return;
  }
  // Proceed with connection
});

Authentication After Connect

WebSockets do not automatically carry cookies or authentication headers. Authenticate after the connection opens:

// Client: Send token after connect
ws.onopen = () => {
  ws.send(JSON.stringify({ 
    type: "auth", 
    token: localStorage.getItem("authToken") 
  }));
};

// Server: Validate on each message
ws.on("message", (message) => {
  const data = JSON.parse(message);
  if (data.type === "auth") {
    const user = validateToken(data.token);
    if (!user) {
      ws.close(4001, "Invalid token");
      return;
    }
    ws.user = user;
  }
});

Rate Limiting

Prevent abuse by implementing message rate limits:

wss.on("connection", (ws) => {
  let messageCount = 0;
  const resetTime = Date.now() + 1000;

  ws.on("message", (message) => {
    const now = Date.now();
    if (now > resetTime) {
      messageCount = 0;
    }
    
    messageCount++;
    if (messageCount > 100) {
      ws.close(4000, "Rate limit exceeded");
    }
  });
});

Common Patterns

Heartbeat / Ping-Pong

Keep connections alive and detect dead connections:

// Client: Send ping every 30 seconds
const pingInterval = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ping" }));
  }
}, 30000);

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === "pong") {
    console.log("Server responsive");
  }
};

// Server: Respond to pings
ws.on("message", (message) => {
  const data = JSON.parse(message);
  if (data.type === "ping") {
    ws.send(JSON.stringify({ type: "pong" }));
  }
});

Optimistic Updates

Update the UI immediately, then reconcile with server response:

function sendMessage(text) {
  // Optimistically add to UI
  addMessageToUI({ text, status: "sending" });
  
  // Send to server
  ws.send(JSON.stringify({ type: "message", text }));
  
  // Server confirms or corrects
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "message-ack") {
      updateMessageStatus(data.id, "sent");
    } else if (data.type === "message-error") {
      updateMessageStatus(data.id, "error", data.reason);
    }
  };
}

When to Use WebSockets

WebSockets excel for:

  • Real-time chat applications
  • Live notifications and feeds
  • Collaborative editing (like Google Docs)
  • Multiplayer games
  • Live dashboards with frequent updates

Consider HTTP long-polling or Server-Sent Events when:

  • Communication is mostly one-directional (server → client)
  • Updates are infrequent
  • You need maximum compatibility with proxies and firewalls
  • Simple request-response workflows suffice

See Also