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
| Condition | Timeout | Behavior |
|---|---|---|
| No ping received | 60 seconds | Server closes the connection |
| No data sent or received | 60 seconds | Server closes the connection |
| Maximum connection duration | None | Connections 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:
| Attempt | Delay |
|---|---|
| 1 | 1 second |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 | 8 seconds |
| 5 | 16 seconds |
| 6+ | 30 seconds (cap) |
State Recovery After Reconnect
After reconnecting:
- Re-subscribe to all channels. The server does not persist subscriptions across connections.
- 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. - Reconcile open orders. Query
openOrdersvia the info endpoint to ensure your local order state matches the server.
Close Codes
| Code | Meaning | Action |
|---|---|---|
1000 | Normal closure | Reconnect if unintended |
1001 | Going away (server shutdown) | Reconnect with backoff |
1006 | Abnormal closure (no close frame) | Reconnect with backoff |
1008 | Policy violation (e.g., rate limit) | Wait longer before reconnecting |
1011 | Server error | Reconnect with backoff |
Best Practices
- Send pings every 50 seconds. This provides a 10-second margin before the 60-second timeout.
- Monitor pong responses. If a pong is not received within 10 seconds of a ping, assume the connection is dead and reconnect proactively.
- Track subscription state. Maintain a list of active subscriptions so they can be restored after reconnection.
- Use a single connection per application. Multiple subscriptions can share one connection. Only open additional connections if you hit the 100-subscription limit.
- Handle partial messages. WebSocket libraries handle framing, but ensure your JSON parser can handle concatenated frames gracefully.