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();
}
}
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
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();
}
}
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);
}
}
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
}
}
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;
}
}
}
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);
}
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');
}
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');
}
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
Bybit
- Heartbeat: Send ping every 20 seconds
- Mandatory disconnect: Every 24 hours
-
Sequence tracking: Use
seqfield 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();
};
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);
};
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();
}
};
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
- WebSocket Reconnect Config Generator — Generate production-ready reconnect strategies with backoff for Binance, Bybit, Kraken, KuCoin, and OKX
- WebSocket Close Code Lookup — Decode WebSocket close codes including 1006, 4000-4999 exchange-specific codes
- Exchange Error Lookup — Look up Binance, Bybit, Kraken, KuCoin, and OKX error codes with recovery actions
- Timestamp Drift Checker — Check clock drift that causes signature errors on signed requests
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)