Skip to main content
Back to Blog
Architecture

Real-Time WebSocket Architecture: Patterns That Actually Scale

April 18, 202611 min read
WebSocketReal-TimeTypeScriptTradingArchitecturePatterns

Real-Time WebSocket Architecture: Patterns That Actually Scale

REST is great until you need data in real-time. Trading platforms, live dashboards, and collaborative tools all need WebSocket connections that don't drop, don't lag, and don't crash your server.

Here's what I've learned building real-time features for the Nexural trading platform.

The Connection Lifecycle

Every WebSocket connection goes through 5 states:

CONNECTING → OPEN → SUBSCRIBED → RECEIVING → CLOSED
     │                                         │
     └──── RECONNECTING ◄─────────────────────┘

Most tutorials stop at OPEN. Production systems need all 5.

Pattern 1: Exponential Backoff Reconnection

Never reconnect immediately. Never reconnect with a fixed interval. Use exponential backoff with jitter:

class ReconnectingWebSocket {
  private retryCount = 0;
  private maxRetries = 10;
  private baseDelay = 1000; // 1 second

  private getDelay(): number {
    const exponential = Math.min(
      this.baseDelay * Math.pow(2, this.retryCount),
      30000 // Cap at 30 seconds
    );
    // Add jitter: ±25% randomization
    const jitter = exponential * (0.75 + Math.random() * 0.5);
    return Math.floor(jitter);
  }

  reconnect() {
    if (this.retryCount >= this.maxRetries) {
      this.fallbackToPolling();
      return;
    }
    const delay = this.getDelay();
    console.log(\`Reconnecting in \${delay}ms (attempt \${this.retryCount + 1})\`);
    setTimeout(() => this.connect(), delay);
    this.retryCount++;
  }
}

Why jitter matters: If your server goes down and 1,000 clients all reconnect at the exact same time with the same backoff schedule, you create a thundering herd that brings the server down again. Jitter spreads the reconnections.

Pattern 2: Heartbeat / Ping-Pong

WebSocket connections can silently die. The TCP connection stays open but no data flows. Heartbeats detect this:

// Client sends ping every 30 seconds
private startHeartbeat() {
  this.heartbeatInterval = setInterval(() => {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
      // If no pong within 5 seconds, connection is dead
      this.pongTimeout = setTimeout(() => {
        this.ws.close();
        this.reconnect();
      }, 5000);
    }
  }, 30000);
}

// Server responds with pong
private handleMessage(data: any) {
  if (data.type === 'pong') {
    clearTimeout(this.pongTimeout);
    return;
  }
  // Handle actual data...
}

Pattern 3: Subscription Management

Don't dump all data through one connection. Use topic-based subscriptions:

// Client subscribes to specific symbols
ws.send(JSON.stringify({
  action: 'subscribe',
  symbols: ['AAPL', 'TSLA', 'ES', 'NQ']
}));

// Server only sends data for subscribed symbols
// Client can unsubscribe without reconnecting
ws.send(JSON.stringify({
  action: 'unsubscribe',
  symbols: ['TSLA']
}));

This reduces bandwidth, simplifies client-side filtering, and lets the server optimize which data streams to maintain.

Pattern 4: Graceful Degradation

When WebSockets fail completely, fall back to HTTP polling. Don't show the user an error — show them slightly stale data:

class MarketDataProvider {
  private mode: 'websocket' | 'polling' = 'websocket';

  async getData(symbol: string) {
    if (this.mode === 'websocket') {
      return this.wsData[symbol]; // Real-time
    }
    // Polling fallback: fetch every 5 seconds
    return fetch(\`/api/quotes/\${symbol}\`).then(r => r.json());
  }

  onWebSocketFail() {
    this.mode = 'polling';
    this.startPolling();
    // Show subtle indicator: "Data delayed ~5s"
  }
}

What I'd Do Differently

  1. Use a message queue (Redis Pub/Sub) between the data source and WebSocket server. Direct connections to market data APIs create tight coupling.
  2. Implement client-side message buffering. If the UI is busy rendering, buffer incoming messages and process them in the next animation frame.
  3. Add connection quality metrics. Track latency per connection and alert when it degrades before the user notices.

Real-time is hard because it fails in ways that are hard to reproduce. Build for failure from day one.

Want to see this in action?

Check out the projects and case studies behind these articles.