For DevelopersAPIWebSocketTimeouts & Heartbeats

Timeouts and Heartbeats

Proper connection management is critical for reliable WebSocket operation. This page covers the heartbeat protocol, timeout behavior, and reconnection strategies.

Heartbeat Protocol

The GX Exchange WebSocket server requires periodic heartbeat messages to keep connections alive.

Ping/Pong

Send a ping message every 50 seconds to prevent the server from closing the connection:

{ "method": "ping" }

The server responds with:

{ "method": "pong" }

Timeout Behavior

ConditionTimeoutBehavior
No ping received60 secondsServer closes the connection
No data sent or received60 secondsServer closes the connection
Maximum connection durationNoneConnections can remain open indefinitely with heartbeats

If the server does not receive any message (including pings) within 60 seconds, it will terminate the connection with a WebSocket close frame.

Implementation

TypeScript

class GxWebSocket {
  private ws: WebSocket;
  private pingInterval: NodeJS.Timeout | null = null;
  private reconnectDelay = 1000;
  private maxReconnectDelay = 30000;
  private url: string;
 
  constructor(url: string) {
    this.url = url;
    this.ws = this.connect();
  }
 
  private connect(): WebSocket {
    const ws = new WebSocket(this.url);
 
    ws.onopen = () => {
      console.log("Connected");
      this.reconnectDelay = 1000; // Reset backoff
      this.startHeartbeat();
    };
 
    ws.onclose = (event) => {
      console.log(`Disconnected: code=${event.code} reason=${event.reason}`);
      this.stopHeartbeat();
      this.scheduleReconnect();
    };
 
    ws.onerror = (error) => {
      console.error("WebSocket error:", error);
    };
 
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.method === "pong") return; // Heartbeat response
      this.handleMessage(msg);
    };
 
    return ws;
  }
 
  private startHeartbeat() {
    this.pingInterval = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ method: "ping" }));
      }
    }, 50000); // 50 seconds
  }
 
  private stopHeartbeat() {
    if (this.pingInterval) {
      clearInterval(this.pingInterval);
      this.pingInterval = null;
    }
  }
 
  private scheduleReconnect() {
    console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
    setTimeout(() => {
      this.ws = this.connect();
      // Re-subscribe to channels after reconnect
      this.resubscribe();
    }, this.reconnectDelay);
 
    // Exponential backoff with cap
    this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
  }
 
  private resubscribe() {
    // Re-subscribe to all previously active channels
  }
 
  private handleMessage(msg: any) {
    // Process incoming data
  }
}

Python

import asyncio
import json
import websockets
 
class GxWebSocket:
    def __init__(self, url: str):
        self.url = url
        self.subscriptions: list[dict] = []
        self.reconnect_delay = 1.0
        self.max_reconnect_delay = 30.0
 
    async def connect(self):
        while True:
            try:
                async with websockets.connect(self.url) as ws:
                    self.reconnect_delay = 1.0  # Reset backoff
                    await self._resubscribe(ws)
 
                    # Run heartbeat and message handler concurrently
                    await asyncio.gather(
                        self._heartbeat(ws),
                        self._receive(ws),
                    )
            except (websockets.ConnectionClosed, ConnectionError) as e:
                print(f"Disconnected: {e}. Reconnecting in {self.reconnect_delay}s...")
                await asyncio.sleep(self.reconnect_delay)
                self.reconnect_delay = min(
                    self.reconnect_delay * 2,
                    self.max_reconnect_delay,
                )
 
    async def _heartbeat(self, ws):
        while True:
            await ws.send(json.dumps({"method": "ping"}))
            await asyncio.sleep(50)  # 50 seconds
 
    async def _receive(self, ws):
        async for message in ws:
            msg = json.loads(message)
            if msg.get("method") == "pong":
                continue
            self.handle_message(msg)
 
    async def _resubscribe(self, ws):
        for sub in self.subscriptions:
            await ws.send(json.dumps({
                "method": "subscribe",
                "subscription": sub,
            }))
 
    def handle_message(self, msg: dict):
        print(f"[{msg.get('channel')}] {msg.get('data')}")

Reconnection Strategy

Exponential Backoff

When a connection is lost, reconnect with exponential backoff:

AttemptDelay
11 second
22 seconds
34 seconds
48 seconds
516 seconds
6+30 seconds (cap)

State Recovery After Reconnect

After reconnecting:

  1. Re-subscribe to all channels. The server does not persist subscriptions across connections.
  2. Request a fresh snapshot. For channels like l2Book, the first message after subscribing is a full snapshot. Do not attempt to apply incremental updates from the previous connection.
  3. Reconcile open orders. Query openOrders via the info endpoint to ensure your local order state matches the server.

Close Codes

CodeMeaningAction
1000Normal closureReconnect if unintended
1001Going away (server shutdown)Reconnect with backoff
1006Abnormal closure (no close frame)Reconnect with backoff
1008Policy violation (e.g., rate limit)Wait longer before reconnecting
1011Server errorReconnect with backoff

Best Practices

  1. Send pings every 50 seconds. This provides a 10-second margin before the 60-second timeout.
  2. Monitor pong responses. If a pong is not received within 10 seconds of a ping, assume the connection is dead and reconnect proactively.
  3. Track subscription state. Maintain a list of active subscriptions so they can be restored after reconnection.
  4. Use a single connection per application. Multiple subscriptions can share one connection. Only open additional connections if you hit the 100-subscription limit.
  5. Handle partial messages. WebSocket libraries handle framing, but ensure your JSON parser can handle concatenated frames gracefully.