
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.
This post is in the Crypto Automation hub and the Crypto Automation category.
- 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
| 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.
Layer 1: Automatic Reconnection
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');
}Shipped asset: WebSocket reconnection kit
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.
- 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.
- REST-only polling architecture.
- Paper trading or backtesting only.
- Learning or prototyping stage.
Included files:
websocket-manager.ts- Full TypeScript WebSocket managerreconnection-checklist.md- Implementation and audit checklistREADME.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
// Binance listen key refresh
setInterval(async () => {
await exchange.keepAliveListenKey(listenKey);
}, 30 * 60 * 1000); // Every 30 minutesBybit
- 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
Common Mistakes
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
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
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

Agent keeps calling same tool: why autonomous agents loop forever in production
When agent loops burn tokens calling same tool repeatedly and cost spikes: why autonomous agents loop without stop rules, and the guardrails that prevent repeat execution and duplicate side effects.

Retries amplify failures: why exponential backoff without jitter creates storms
When retries make dependency failures worse and 429s multiply: why exponential backoff without jitter creates synchronized waves, and the bounded retry policy that stops amplification.

Trading bot keeps getting 429s after deploy: stop rate limit storms
When deploys trigger 429 storms: why synchronized restarts amplify rate limits, how to diagnose fixed window vs leaky bucket, and guardrails that stop repeat incidents.