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()
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:
- Detect the disconnect (more on that below)
- Reconnect with backoff
- Fetch a full REST snapshot of the relevant state (order book, positions, whatever your bot tracks)
- Apply the snapshot to reset local state entirely
- Resubscribe to the WebSocket stream
- 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)
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")
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)