Connection Lifecycle
WebSocket connection states, close codes, ping/pong heartbeat, reconnection strategies, and state recovery after reconnect.
Understanding the WebSocket connection lifecycle helps you build robust clients that handle all connection states gracefully.
Connection States
A WebSocket connection transitions through these states:
| State | Description |
|---|---|
| Connecting | TCP handshake and WebSocket upgrade in progress |
| Open | Connection established, ready for messaging |
| Closing | Close frame sent, waiting for acknowledgment |
| Closed | Connection terminated |
Connecting ──► Open ──► Closing ──► Closed
│ ▲
└──────────────────────┘
(on error/timeout)Close Codes
When the server closes a connection, it sends a close code indicating the reason.
Standard Close Codes
| Code | Name | Description |
|---|---|---|
| 1000 | Normal Closure | Clean shutdown, no error |
| 1001 | Going Away | Server shutting down or restarting |
| 1008 | Policy Violation | Authentication failure or limit exceeded |
| 1011 | Internal Error | Unexpected server error |
Interpreting Close Codes
ws.onclose = (event) => {
switch (event.code) {
case 1000:
console.log('Connection closed normally');
break;
case 1001:
console.log('Server going away, reconnect shortly');
scheduleReconnect();
break;
case 1008:
console.error('Policy violation:', event.reason);
// Check reason for specific cause
break;
case 1011:
console.error('Server error, will retry');
scheduleReconnect();
break;
default:
console.error('Unexpected close:', event.code, event.reason);
}
};Listing Subscriptions
You can request a list of your current subscriptions at any time:
Request:
{
"type": "list_subscriptions"
}Response:
{
"type": "subscriptions",
"count": 3,
"max": 100,
"subscriptions": [
{
"id": "candles.binancef.btc/usd.60",
"channel": "candles",
"exchange": "binancef",
"symbol": "btc/usd",
"tf": "1m"
},
{
"id": "trades.binancef.eth/usd",
"channel": "trades",
"exchange": "binancef",
"symbol": "eth/usd"
},
{
"id": "vd.bybitf.btc/usd.60.1",
"channel": "vd",
"exchange": "bybitf",
"symbol": "btc/usd",
"tf": "1m",
"bucket": 1
}
]
}| Field | Type | Description |
|---|---|---|
type | string | Always "subscriptions" |
count | int | Number of active subscriptions |
max | int | Maximum subscriptions allowed |
subscriptions | array | List of subscription details |
Server-Initiated Disconnections
The server may close your connection for these reasons:
API Key Revoked
If your API key is revoked or disabled while connected, the server closes the connection with code 1008.
Max Connections Exceeded
Each API key has a connection limit. Exceeding it results in the oldest connection being closed with code 1008 and reason max connections exceeded.
Backpressure (Slow Client)
If your client cannot consume messages fast enough, the server's send buffer fills up. Rather than consume unbounded memory, the server closes slow connections with code 1008 and reason backpressure.
Prevention:
- Process messages asynchronously
- Don't block the message handler
- Subscribe only to channels you need
- Consider reducing subscription count if issues persist
Disconnecting Message
Before closing a connection, the server sends a disconnecting message with the reason:
{
"type": "disconnecting",
"reason": "backpressure"
}| Reason | Description |
|---|---|
backpressure | Client too slow to consume messages |
timeout | Connection idle too long |
server_shutdown | Server is restarting or shutting down |
normal | Clean client-initiated disconnect |
This message is sent immediately before the WebSocket close frame, giving your client a chance to log or handle the disconnect reason.
Server Maintenance
During deployments or restarts, you receive code 1001 (Going Away). Reconnect after a brief delay.
Ping/Pong Heartbeat
The server supports two types of heartbeat mechanisms:
Protocol-Level Ping/Pong
The server sends WebSocket ping frames periodically to detect dead connections.
- Server sends ping frame every 30 seconds
- Client must respond with pong frame
- If no pong received within timeout, server assumes connection dead
- Server closes the connection
Most WebSocket libraries handle protocol-level ping/pong automatically:
// Node.js ws library - automatic
const ws = new WebSocket(url);
// Pong is sent automatically
// Browser WebSocket API - automatic
// No action needed, browser handles it# Python websockets - automatic
# Pong frames are sent automatically
# If using a library that doesn't auto-respond:
async for message in websocket:
if isinstance(message, websockets.frames.Ping):
await websocket.pong(message.data)// gorilla/websocket - must configure
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})Application-Level Ping/Pong
For additional reliability, you can also use application-level ping/pong messages:
Ping Command:
{
"type": "ping"
}Pong Response:
{
"type": "pong"
}This is useful for:
- Libraries that don't expose protocol-level ping/pong
- Measuring application-level latency
- Keeping connections alive through proxies that may not forward WebSocket frames
Reconnection Strategy
Implement exponential backoff to avoid overwhelming the server during outages.
Recommended Algorithm
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.minDelay = options.minDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.factor = options.factor || 2;
this.currentDelay = this.minDelay;
this.subscriptions = new Set();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.currentDelay = this.minDelay; // Reset backoff
this.resubscribe(); // Restore subscriptions after reconnect
};
this.ws.onclose = (event) => {
if (event.code !== 1000) {
this.scheduleReconnect();
}
};
this.ws.onerror = () => {
// onclose will fire after this
};
}
scheduleReconnect() {
const jitter = Math.random() * 0.3 * this.currentDelay;
const delay = this.currentDelay + jitter;
console.log(`Reconnecting in ${Math.round(delay)}ms`);
setTimeout(() => this.connect(), delay);
// Increase delay for next attempt
this.currentDelay = Math.min(
this.currentDelay * this.factor,
this.maxDelay
);
}
resubscribe() {
for (const channel of this.subscriptions) {
this.ws.send(JSON.stringify({
type: 'subscribe',
channel: channel
}));
}
}
subscribe(channel) {
this.subscriptions.add(channel);
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'subscribe',
channel: channel
}));
}
}
unsubscribe(channel) {
this.subscriptions.delete(channel);
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'unsubscribe',
channel: channel
}));
}
}
}Backoff Schedule
| Attempt | Delay (base) | With Jitter (typical) |
|---|---|---|
| 1 | 1s | 1.0 - 1.3s |
| 2 | 2s | 2.0 - 2.6s |
| 3 | 4s | 4.0 - 5.2s |
| 4 | 8s | 8.0 - 10.4s |
| 5 | 16s | 16.0 - 20.8s |
| 6+ | 30s (max) | 30.0 - 39.0s |
Handling Network Drops
Network disconnections don't always produce clean close events. Implement additional detection.
Client-Side Timeout
If you expect regular data but receive nothing, the connection may be dead:
class ConnectionMonitor {
constructor(ws, timeout = 60000) {
this.ws = ws;
this.timeout = timeout;
this.timer = null;
}
start() {
this.resetTimer();
}
// Call this whenever you receive any message
onMessage() {
this.resetTimer();
}
resetTimer() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
console.warn('No data received, connection may be dead');
this.ws.close();
}, this.timeout);
}
stop() {
clearTimeout(this.timer);
}
}
// Usage
const monitor = new ConnectionMonitor(ws, 60000);
monitor.start();
ws.onmessage = (event) => {
monitor.onMessage();
// Process message...
};
ws.onclose = () => {
monitor.stop();
};Application-Level Heartbeat
For additional reliability, implement your own heartbeat:
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
// Server responds with { type: 'pong' }Graceful Shutdown
When your client needs to disconnect cleanly:
function disconnect() {
// Unsubscribe from all channels (optional but clean)
for (const channel of subscriptions) {
ws.send(JSON.stringify({
type: 'unsubscribe',
channel: channel
}));
}
// Close with normal closure code
ws.close(1000, 'Client disconnecting');
}Shutdown Sequence
- Client sends close frame (code 1000)
- Server receives close frame
- Server sends close frame acknowledgment
- Both sides close TCP connection
onclosefires with code 1000
State Recovery After Reconnect
After reconnecting, your client needs to restore its previous state. Note that authentication happens automatically via the api_key query parameter in your WebSocket URL - there is no separate authentication message.
ws.onopen = async () => {
// Authentication is automatic via URL query param
// Just resubscribe to all channels
for (const channel of savedSubscriptions) {
await subscribe(channel);
}
console.log('Subscriptions restored');
};Important Considerations
- Trades: You may miss trades during disconnect. If critical, use REST API to fetch missed trades.
- Depth (Orderbook): On reconnect, clear your local orderbook state. The
depthchannel sends incremental updates (only changed levels) every ~250ms and full snapshots every ~60 seconds. Apply updates by setting levels (qty > 0) or removing them (qty = 0). - Candles: Request latest candle state; partial updates may have been missed.
Best Practices Summary
- Always implement reconnection with exponential backoff
- Maintain subscription list client-side for resubscription
- Handle all close codes appropriately
- Verify ping/pong is handled by your WebSocket library
- Implement timeout detection for network drops
- Request snapshots after reconnect for stateful data
- Add jitter to backoff to prevent thundering herd
- Log connection events for debugging