WebSocket Disconnects in Trading Bots: Reconnection That Actually Works

Feb 25, 20264 min read

Share|

Category:AutomationCrypto

WebSocket Disconnects in Trading Bots: Reconnection That Actually Works

Handle WebSocket disconnects in trading bots with automatic reconnection, message gap detection, and state recovery—without missing fills or duplicating orders.

Free download: WebSocket Reconnection Kit. Jump to the download section.

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.

Fast Triage: Disconnect Recovery Strategies

ScenarioStrategyRecovery Time
Clean disconnect (server close)Immediate reconnect1-2s
Network glitchReconnect with backoff2-10s
Message gap detectedREST snapshot + resubscribe5-15s
Extended outageTimed backoff, reconciliation30s-5min
Auth token expiredRe-authenticate + reconnect3-10s

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

The Three-Layer Defense

Reliable WebSocket handling requires three layers:

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

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


Layer 1: Automatic Reconnection

Basic Reconnection Pattern

typescript
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();
  }
}

Handling Exchange-Specific Close Codes

Exchanges send close codes that indicate why connection dropped:

CodeMeaningResponse
1000Normal closeReconnect immediately
1001Going away (server restart)Reconnect immediately
1006Abnormal close (no close frame)Reconnect with backoff
1008Policy violationCheck auth, then reconnect
1011Server errorReconnect with backoff
4000-4999Exchange-specificCheck exchange docs
typescript
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);
  }
}

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:

typescript
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
  }
}

Heartbeat/Ping-Pong

Detect stale connections before they cause problems:

typescript
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;
    }
  }
}

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

typescript
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);
}

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

typescript
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');
}

Full Reconnection Flow

typescript
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');
}

Shipped asset: WebSocket reconnection kit

Download
Free

WebSocket Reconnection Kit

TypeScript WebSocket manager with automatic reconnection, gap detection, heartbeat handling, and state recovery. Works with Binance, Bybit, OKX, and ccxt-compatible exchanges.

When to use this (fit check)
  • Your bot uses WebSocket for order updates or market data.
  • You have experienced missed fills or state drift.
  • You trade on any exchange with mandatory reconnection windows.
When NOT to use this (yet)
  • REST-only polling architecture.
  • Paper trading or backtesting only.
  • Learning or prototyping stage.

Included files:

  • websocket-manager.ts - Full TypeScript WebSocket manager
  • reconnection-checklist.md - Implementation and audit checklist
  • README.md - Integration guide

Exchange-Specific Notes

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
typescript
// Binance listen key refresh
setInterval(async () => {
  await exchange.keepAliveListenKey(listenKey);
}, 30 * 60 * 1000); // Every 30 minutes

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

Common Mistakes

Mistake 1: Trusting WebSocket Alone

typescript
// ❌ 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();
};

Mistake 2: Reconnecting Too Fast

typescript
// ❌ Wrong: Instant reconnect
ws.onclose = () => ws.reconnect();
 
// ✅ Correct: Exponential backoff
ws.onclose = () => {
  const delay = calculateBackoff(attempts);
  setTimeout(() => ws.reconnect(), delay);
};

Mistake 3: Ignoring Close Codes

typescript
// ❌ 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();
  }
};

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

Every 15-30 seconds for most exchanges. Binance recommends every 30 seconds, Bybit every 20 seconds. Too frequent wastes bandwidth, too slow means delayed stale detection. Check your exchange's documentation for their recommended interval.

For Node.js, the ws package is standard. Don't use socket.io for exchange connections—it adds protocol overhead exchanges don't support. For browser, native WebSocket works. Libraries like reconnecting-websocket help but don't handle exchange-specific requirements like auth and message gap detection.

REST wins. WebSocket messages can be delayed, reordered, or lost. REST is a snapshot of truth. If you fetch an order via REST and it shows filled but WebSocket still shows open, trust REST and update your local state accordingly.

Use idempotency keys. If disconnect happens after sending order but before confirmation, reconnect and query by client order ID. This tells you if the order went through. Never re-place without checking first—you'll double your position.


Resources

Coming soon

Axiom is coming

Join the waitlist and get notified when we ship real, operational tooling (not tutorials).

Recommended resources

Download the shipped checklist/templates for this post.

WebSocket manager template with automatic reconnection, gap detection, and state recovery for trading bots.

resource

Related posts