DEV Community

Cover image for WebSocket Reconnection That Actually Works: Auto-Reconnect Guide for Trading Bots
Kamran
Kamran

Posted on • Originally published at matrixtrak.com

WebSocket Reconnection That Actually Works: Auto-Reconnect Guide for Trading Bots

This post was originally published on MatrixTrak.com — the production reliability toolkit for trading bot operators and .NET engineers.

Complete WebSocket auto-reconnect guide for trading bots. Implement automatic reconnection with exponential backoff, heartbeat ping-pong, message gap detection, and state recovery.
WebSocket connections drop. Not maybe. Definitely. Exchanges reset connections every 24 hours. Networks glitch. Load balancers rotate. HTTP proxies timeout. Your trading bot will experience disconnects.

The question isn't whether you'll disconnect—it's whether your bot recovers correctly when you do.

If you only do three things

  • Implement automatic reconnection with exponential backoff and jitter.
  • Track sequence numbers to detect missed messages.
  • Always verify state via REST after reconnect. Never trust WebSocket alone.

WebSocket Auto-Reconnect Quick Start

If you just need a working auto-reconnect loop right now, here's the minimum viable implementation:

class AutoReconnectWebSocket {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxRetries = 10;
  private shouldReconnect = true;

  async connect(url: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(url);
      this.ws.onopen = () => {
        this.reconnectAttempts = 0;
        resolve();
      };
      this.ws.onclose = (event) => {
        if (this.shouldReconnect) {
          const delay = this.backoff();
          console.log(`[AutoReconnect] Closed ${event.code}. Reconnecting in ${delay}ms`);
          setTimeout(() => this.connect(url), delay);
        }
      };
      this.ws.onerror = () => { /* onclose fires next */ };
    });
  }

  private backoff(): number {
    const base = 1000;
    const max = 30000;
    const delay = Math.min(base * Math.pow(2, this.reconnectAttempts++), max);
    return delay + delay * 0.2 * (Math.random() * 2 - 1); // +20% jitter
  }

  close(): void {
    this.shouldReconnect = false;
    this.ws?.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

This handles the core auto-reconnect loop: exponential backoff with jitter, max retry limit, and clean shutdown. The sections below add gap detection, heartbeat monitoring, and state recovery on top of this foundation.

Fast Triage: Disconnect Recovery Strategies

Scenario Strategy Recovery Time
Clean disconnect (server close) Immediate reconnect 1-2s
Network glitch Reconnect with backoff 2-10s
Message gap detected REST snapshot + resubscribe 5-15s
Extended outage Timed backoff, reconciliation 30s-5min
Auth token expired Re-authenticate + reconnect 3-10s

Start with clean disconnect handling. It covers 80% of cases.

The Three-Layer Defense

Reliable WebSocket handling requires three layers:

Layer 1: Reconnection     │ Get connection back
Layer 2: Gap Detection    │ Know if you missed messages
Layer 3: State Recovery   │ Fix state if you did
Enter fullscreen mode Exit fullscreen mode

Skip any layer and you're vulnerable to silent data loss.


Basic Reconnection Pattern

class WebSocketManager {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private isIntentionallyClosed = false;

  async connect(url: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(url);

      this.ws.onopen = () => {
        console.log('[WS] Connected');
        this.reconnectAttempts = 0; // Reset on success
        resolve();
      };

      this.ws.onclose = (event) => {
        console.log(`[WS] Closed: code=${event.code}, reason=${event.reason}`);

        if (!this.isIntentionallyClosed) {
          this.scheduleReconnect(url);
        }
      };

      this.ws.onerror = (error) => {
        console.error('[WS] Error:', error);
        // onclose will fire after onerror, reconnect happens there
      };

      this.ws.onmessage = (event) => {
        this.handleMessage(JSON.parse(event.data));
      };
    });
  }

  private scheduleReconnect(url: string): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('[WS] Max reconnect attempts reached');
      this.onMaxRetriesExceeded();
      return;
    }

    const delay = this.calculateBackoff(this.reconnectAttempts);
    this.reconnectAttempts++;

    console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);

    setTimeout(() => this.connect(url), delay);
  }

  private calculateBackoff(attempt: number): number {
    // Exponential backoff: 1s, 2s, 4s, 8s... capped at 30s
    const base = 1000;
    const max = 30000;
    const delay = Math.min(base * Math.pow(2, attempt), max);

    // Add jitter (±20%) to prevent thundering herd
    const jitter = delay * 0.2 * (Math.random() * 2 - 1);
    return Math.floor(delay + jitter);
  }

  close(): void {
    this.isIntentionallyClosed = true;
    this.ws?.close();
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling Exchange-Specific Close Codes

Exchanges send close codes that indicate why connection dropped:

Code Meaning Response
1000 Normal close Reconnect immediately
1001 Going away (server restart) Reconnect immediately
1006 Abnormal close (no close frame) Reconnect with backoff
1008 Policy violation Check auth, then reconnect
1011 Server error Reconnect with backoff
4000-4999 Exchange-specific Check exchange docs
private handleClose(event: CloseEvent, url: string): void {
  switch (event.code) {
    case 1000:
    case 1001:
      // Server closed cleanly, reconnect fast
      setTimeout(() => this.connect(url), 100);
      break;

    case 1008:
      // Auth issue, refresh token first
      this.refreshAuth().then(() => this.connect(url));
      break;

    default:
      // Unknown or error, use backoff
      this.scheduleReconnect(url);
  }
}
Enter fullscreen mode Exit fullscreen mode

Layer 2: Message Gap Detection

Disconnects are obvious. Message loss is silent. You need to detect when messages were dropped without a disconnect.

Sequence Number Tracking

Most exchanges include sequence numbers in messages:

interface ExchangeMessage {
  e: string;        // Event type
  E: number;        // Event time
  u?: number;       // Update ID / sequence number
  // ... other fields
}

class GapDetector {
  private lastSequence: number | null = null;
  private gapDetected = false;

  checkSequence(message: ExchangeMessage): void {
    if (message.u === undefined) return; // Not all messages have sequences

    if (this.lastSequence !== null) {
      const expected = this.lastSequence + 1;

      if (message.u > expected) {
        console.warn(`[Gap] Missed ${message.u - expected} messages`);
        this.gapDetected = true;
        this.onGapDetected(this.lastSequence, message.u);
      } else if (message.u < expected) {
        console.warn(`[Gap] Duplicate or out-of-order message`);
        // Usually safe to ignore, but log for debugging
      }
    }

    this.lastSequence = message.u;
  }

  reset(): void {
    this.lastSequence = null;
    this.gapDetected = false;
  }

  private onGapDetected(lastSeen: number, current: number): void {
    // Trigger state recovery
  }
}
Enter fullscreen mode Exit fullscreen mode

Heartbeat/Ping-Pong

Detect stale connections before they cause problems:

class HeartbeatManager {
  private pingInterval: NodeJS.Timer | null = null;
  private lastPong: number = Date.now();
  private timeout = 30000; // 30 seconds without pong = stale

  start(ws: WebSocket, interval: number = 15000): void {
    this.pingInterval = setInterval(() => {
      if (Date.now() - this.lastPong > this.timeout) {
        console.warn('[Heartbeat] Connection stale, forcing reconnect');
        ws.close(4001, 'Heartbeat timeout');
        return;
      }

      ws.send(JSON.stringify({ op: 'ping' }));
    }, interval);
  }

  receivedPong(): void {
    this.lastPong = Date.now();
  }

  stop(): void {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Some exchanges (Binance) require you to respond to their pings:

private handleMessage(data: any): void {
  // Binance style ping
  if (data.ping) {
    this.ws?.send(JSON.stringify({ pong: data.ping }));
    return;
  }

  // OKX style ping
  if (data === 'ping') {
    this.ws?.send('pong');
    return;
  }

  // Process actual message
  this.processMessage(data);
}
Enter fullscreen mode Exit fullscreen mode

Layer 3: State Recovery

When you detect a gap or reconnect after an outage, you need to recover state. Never trust that WebSocket will catch you up.

REST Snapshot After Reconnect

async function recoverStateAfterReconnect(
  exchange: Exchange,
  state: TradingState
): Promise<void> {
  console.log('[Recovery] Starting post-reconnect state recovery...');

  // 1. Fetch current open orders via REST
  const exchangeOrders = await exchange.fetchOpenOrders(state.symbol);

  // 2. Reconcile with local state
  for (const remote of exchangeOrders) {
    const local = state.findOrder(remote.clientOrderId);

    if (!local) {
      // Orphan: was created while disconnected
      console.log(`[Recovery] Adopting orphan order: ${remote.clientOrderId}`);
      await state.adoptOrder(remote);
    } else if (remote.filled > local.filled) {
      // Fill happened while disconnected
      console.log(`[Recovery] Backfilling: ${local.filled}${remote.filled}`);
      await state.processFill(remote.clientOrderId, remote.filled, remote.price);
    }
  }

  // 3. Check for orders that closed while disconnected
  for (const local of state.openOrders) {
    const remote = exchangeOrders.find(o => o.clientOrderId === local.clientOrderId);

    if (!remote) {
      // Order no longer open—check what happened
      const historical = await exchange.fetchOrder(local.clientOrderId);

      if (historical?.status === 'filled') {
        await state.processFill(local.clientOrderId, historical.filled, historical.price);
      } else if (historical?.status === 'canceled') {
        await state.removeOrder(local.clientOrderId);
      }
    }
  }

  // 4. Verify position
  const position = await exchange.fetchPosition(state.symbol);
  if (Math.abs(position.size - state.position.size) > 0.0001) {
    console.warn(`[Recovery] Position drift corrected: ${state.position.size}${position.size}`);
    state.position.size = position.size;
    state.position.entryPrice = position.entryPrice;
  }

  console.log('[Recovery] State recovery complete');
}
Enter fullscreen mode Exit fullscreen mode

Full Reconnection Flow

async function fullReconnectionSequence(
  wsManager: WebSocketManager,
  exchange: Exchange,
  state: TradingState,
  subscriptions: string[]
): Promise<void> {
  // 1. Reconnect WebSocket
  await wsManager.connect(exchange.wsUrl);

  // 2. Re-authenticate if required
  await wsManager.authenticate(exchange.apiKey, exchange.apiSecret);

  // 3. Recover state via REST (don't trust WS history)
  await recoverStateAfterReconnect(exchange, state);

  // 4. Resubscribe to channels
  for (const sub of subscriptions) {
    await wsManager.subscribe(sub);
  }

  // 5. Reset gap detector (new connection, new sequence)
  gapDetector.reset();

  console.log('[Reconnect] Full reconnection sequence complete');
}
Enter fullscreen mode Exit fullscreen mode


Binance

  • Mandatory disconnect: Every 24 hours
  • Listen key: Expires every 60 minutes, must refresh via REST
  • Ping requirement: Respond to Binance pings or get disconnected
// Binance listen key refresh
setInterval(async () => {
  await exchange.keepAliveListenKey(listenKey);
}, 30 * 60 * 1000); // Every 30 minutes
Enter fullscreen mode Exit fullscreen mode

Bybit

  • Heartbeat: Send ping every 20 seconds
  • Mandatory disconnect: Every 24 hours
  • Sequence tracking: Use seq field in messages

OKX

  • Heartbeat: Respond to "ping" with "pong"
  • Login required: For private channels
  • Channel-based subscriptions: Must resubscribe after reconnect

Mistake 1: Trusting WebSocket Alone

// ❌ Wrong: Assuming WS gives complete history
ws.onopen = () => {
  console.log('Connected, ready to trade');
  startTrading(); // Dangerous!
};

// ✅ Correct: Verify state after reconnect
ws.onopen = async () => {
  await recoverStateAfterReconnect(exchange, state);
  startTrading();
};
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Reconnecting Too Fast

// ❌ Wrong: Instant reconnect
ws.onclose = () => ws.reconnect();

// ✅ Correct: Exponential backoff
ws.onclose = () => {
  const delay = calculateBackoff(attempts);
  setTimeout(() => ws.reconnect(), delay);
};
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Ignoring Close Codes

// ❌ Wrong: Treating all closes the same
ws.onclose = () => reconnect();

// ✅ Correct: Handle codes appropriately
ws.onclose = (event) => {
  if (event.code === 1008) {
    refreshAuth().then(reconnect);
  } else {
    reconnect();
  }
};
Enter fullscreen mode Exit fullscreen mode

Checklist (copy/paste)

Reconnection:

  • [ ] Automatic reconnection on disconnect
  • [ ] Exponential backoff with jitter
  • [ ] Max retry limit with alerting
  • [ ] Close code handling

Gap detection:

  • [ ] Sequence number tracking
  • [ ] Heartbeat/ping-pong implemented
  • [ ] Stale connection detection
  • [ ] Gap triggers state recovery

State recovery:

  • [ ] REST snapshot after reconnect
  • [ ] Order reconciliation
  • [ ] Fill backfill
  • [ ] Position verification

Exchange-specific:

  • [ ] Listen key refresh (Binance)
  • [ ] Ping response implemented
  • [ ] Resubscription after reconnect
  • [ ] Auth token refresh

Monitoring:

  • [ ] Disconnect events logged
  • [ ] Gap events logged
  • [ ] Recovery time tracked
  • [ ] Alert on max retries



Related tools

Continue reading the full article on MatrixTrak →


Free Tools for Trading Bot Reliability

Every article on MatrixTrak is backed by free, open-source tools you can use right now:

  • WebSocket Reconnect Config Generator — production-ready reconnect strategies with backoff → Try it free
  • Retry Policy Generator — backoff/jitter/caps for C#, Python, and YAML → Try it free

Originally published at matrixtrak.com.

Top comments (0)