Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling & Reconnection

Handshake errors

HTTP codeCauseAction
426Malformed upgrade (missing/invalid Sec-WebSocket-Key)Fix the WebSocket client
401Missing X-API-Key headerAdd the header
403Invalid, revoked, or expired keyRequest a new key
429Rate or connection limit hitBack off, see Rate Limits

Close codes

CodeReasonMeaningReconnect?
1000key_expiredKey expiration date passedNo — get a new key
1000key_invalidatedAdministrator revoked the keyNo — get a new key
1008too_slowClient lagged > 10 messages behindYes — process faster
1008rate_limit_exceededSent > 3 messages / minute to the serverYes — stop sending
1009frame_too_largeSent a frame larger than 1 KBYes — fix the client

Reconnection strategy

Exponential backoff, capped at 5 minutes:

attempt 1 → 1 s
attempt 2 → 2 s
attempt 3 → 4 s
attempt 4 → 8 s
…           cap at 300 s

Reset the counter after a successful connection.

Decision logic:

TriggerAction
Close key_expired / key_invalidatedStop
Close rate_limit_exceededWait 60 s, stop sending client messages, reconnect
Close too_slowReconnect immediately, process faster
HTTP 429Back off (you hit a connection limit)
Any other disconnectReconnect with exponential backoff

Detecting dead connections

The server sends a WebSocket PING every 15 s and closes the connection if no PONG arrives within 30 s. Your library handles PONG automatically and surfaces the disconnect as a close event — when the server is there to close it.

The case that bites: the server disappears without saying goodbye (host stopped or migrated, public IP moved, path cut). No FIN, no RST, no PING — nothing arrives, and nothing tells your socket it is dead. A client that only reads stays “connected” indefinitely and misses every announcement from then on. The rule is therefore: no frame of any kind (PING or heartbeat) for 60 s = dead connection → close and reconnect. A healthy connection carries a frame at least every 15 s, so 60 s is four missed server pings, far from a false positive.

LibraryWhat to do
Python websocketsNothing: the default ping_interval=20, ping_timeout=20 sends client pings and raises ConnectionClosed when pongs stop. Keep those defaults.
Node.js wsws does not ping by itself. Reset a 60 s timer on every 'ping' and 'message'; when it fires, ws.terminate() and let your reconnect logic run.
Go gorilla/websocketSetReadDeadline(now + 60 s) and push it forward in the ping handler and on every message.
Rust tokio-tungsteniteA tokio::time::interval in your select! that checks the last-received instant and breaks out of the read loop past 60 s.

Enable TCP keepalive on the socket too if your stack lets you (SO_KEEPALIVE, e.g. 30 s idle / 10 s probes): the kernel then detects a vanished peer even if the application does nothing.

import asyncio, json, websockets

async def stream(ws):
    try:
        async for raw in ws:
            data = json.loads(raw)
            if data["type"] == "announcement":
                handle(data)
    except websockets.ConnectionClosed as e:
        print(f"closed code={e.code} reason={e.reason!r}")

Don’t confuse the two silences. “No announcement for N seconds” is normal — listings are sparse, never reconnect on that. “No frame at all (PING, heartbeat) for 60 s” is a dead connection — always reconnect on that, whether or not your library reported a close.