DEV Community

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

Posted on

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

Your Trading Bot Doesn't Have a WebSocket Problem. It Has a Assumptions Problem.

Most WebSocket reconnection bugs aren't caused by bad reconnection code. They're caused by code that reconnects successfully and then quietly serves stale, gapped, or out-of-order data like nothing happened.

That distinction matters enormously in trading. A bot that crashes on disconnect is annoying. A bot that reconnects and silently processes a corrupted order book is dangerous.

Here's what production-grade reconnection logic actually needs to solve.


The Connection Will Drop. Schedule It.

Exchanges don't treat WebSocket connections as permanent infrastructure. Kraken, for example, recycles connections roughly every 24 hours. Binance will drop you for inactivity. Coinbase Advanced Trade has its own session limits. These aren't failure modes. They're documented behaviors.

Your reconnection logic should treat a drop as a scheduled event, not an exception. That framing matters because it changes how you write the code. You stop writing defensive patches and start writing a proper state machine.

The first thing that state machine needs is exponential backoff with jitter.

import random
import time

def backoff_delay(attempt: int, base: float = 0.5, cap: float = 30.0) -> float:
    delay = min(cap, base * (2 ** attempt))
    jitter = random.uniform(0, delay * 0.3)
    return delay + jitter

attempt = 0
while not connected:
    time.sleep(backoff_delay(attempt))
    attempt += 1
    connected = try_reconnect()
Enter fullscreen mode Exit fullscreen mode

Without jitter, every bot that dropped at the same moment (say, during an exchange-side recycle) hammers the connection endpoint simultaneously. That thundering herd problem can get your IP rate-limited right when you need the connection most. Jitter spreads the load. It's a small addition that prevents a nasty class of cascading failures.


Reconnected Is Not the Same as Recovered

This is where most implementations stop, and where the real problems start.

When you re-establish a WebSocket connection and resubscribe to a channel, you get a fresh stream from that moment forward. You have no idea what happened between your disconnect and your reconnect. Order book updates may have fired. Trades may have executed. Price levels may have changed.

If your bot picks up the stream and assumes local state is still valid, it's now making decisions based on a lie.

The correct recovery sequence is:

  1. Detect the disconnect (more on that below)
  2. Reconnect with backoff
  3. Fetch a full REST snapshot of the relevant state (order book, positions, whatever your bot tracks)
  4. Apply the snapshot to reset local state entirely
  5. Resubscribe to the WebSocket stream
  6. Begin processing live updates against the fresh snapshot

Step 3 is the one that gets skipped. It's slower. It requires a REST call. It feels redundant when you "just need to get back online." But skipping it means your in-memory order book is built on a foundation that may have drifted significantly.


Sequence Numbers Are Your Gap Detector

Some exchanges send sequence numbers or update IDs with their WebSocket messages. Binance does this with order book depth streams. If you're not tracking them, you're leaving a valuable signal on the floor.

last_sequence = None

def process_update(msg):
    global last_sequence
    seq = msg.get("sequence")

    if last_sequence is not None and seq != last_sequence + 1:
        # Gap detected — do not process this update
        trigger_snapshot_recovery()
        return

    last_sequence = seq
    apply_update(msg)
Enter fullscreen mode Exit fullscreen mode

A gap in sequence numbers means you missed messages. It doesn't matter that the WebSocket is still technically open. Your state is already corrupted from the perspective of that stream. Detecting the gap and triggering a snapshot recovery at that point is the same logic you use post-reconnect. It should be the same code path.


Heartbeats Catch What TCP Misses

TCP keepalive is not enough for detecting silent failures. A connection can appear open at the socket level while the exchange's server has already moved on. You won't find out until you try to send a message and the write fails, or until you've gone 60 seconds without a message and finally notice.

Heartbeat ping-pong closes that gap. Most exchanges support WebSocket-level ping frames or application-level heartbeat messages. Send a ping on a regular interval (typically every 10 to 30 seconds) and expect a pong back within a timeout window.

async def heartbeat_loop(ws, interval=20, timeout=10):
    while True:
        await asyncio.sleep(interval)
        try:
            pong = await ws.ping()
            await asyncio.wait_for(pong, timeout=timeout)
        except asyncio.TimeoutError:
            await ws.close()
            raise ConnectionError("Heartbeat timeout — reconnecting")
Enter fullscreen mode Exit fullscreen mode

If the pong doesn't arrive, you close the connection yourself and trigger your reconnection flow. You're now in control of the failure, not reacting to it after the fact.


The Actual Takeaway

Reconnection is not a solved problem just because you catch the disconnect exception and call connect() again. The hard part is everything that happens after the socket handshake: validating state integrity, detecting gaps, fetching snapshots, and using heartbeats to surface silent failures before they become missed trades.

If you'd rather not own all of that logic, Turboline's data streaming infrastructure handles connection resilience at the platform level, delivering gapless, sequenced market data to your bot regardless of what's happening at the exchange transport layer.

But if you're building it yourself, the rule is simple: never trust WebSocket state after a reconnect. Always verify.

Top comments (0)