Timeouts & Heartbeats
Proper timeout and heartbeat management is crucial for maintaining stable WebSocket connections to the Synthetix API.
Connection Lifecycle
- Initial Connection: TCP handshake and WebSocket upgrade
- Authentication: 30-second window for trade endpoint
- Active State: Send/receive messages with heartbeats
- Idle Timeout: Connection closed after inactivity
- Max Duration: Forced disconnect after 24 hours
Heartbeat Protocol
Client Heartbeat (Ping)
Send a ping message every 30 seconds to keep the connection alive:
{
"type": "ping",
"timestamp": 1704067200000
}Server Response (Pong)
The server responds with:
{
"type": "pong",
"timestamp": 1704067200000
}Implementation
class HeartbeatManager {
constructor(ws) {
this.ws = ws;
this.pingInterval = 30000; // 30 seconds
this.pongTimeout = 10000; // 10 seconds
this.intervalId = null;
this.timeoutId = null;
this.lastPong = Date.now();
}
start() {
this.intervalId = setInterval(() => {
this.sendPing();
}, this.pingInterval);
// Listen for pongs
this.ws.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (message.type === 'pong') {
this.handlePong(message);
}
});
}
sendPing() {
if (this.ws.readyState !== WebSocket.OPEN) {
return;
}
const ping = {
type: 'ping',
timestamp: Date.now()
};
this.ws.send(JSON.stringify(ping));
// Set pong timeout
this.timeoutId = setTimeout(() => {
console.error('Pong timeout - connection may be dead');
this.ws.close(1001, 'Pong timeout');
}, this.pongTimeout);
}
handlePong(message) {
clearTimeout(this.timeoutId);
this.lastPong = Date.now();
// Calculate round-trip time
const rtt = Date.now() - message.timestamp;
console.log(`Heartbeat RTT: ${rtt}ms`);
}
stop() {
clearInterval(this.intervalId);
clearTimeout(this.timeoutId);
}
getLastPongAge() {
return Date.now() - this.lastPong;
}
}Timeout Configuration
Connection Timeouts
| Timeout Type | Duration | Description |
|---|---|---|
| Connection | 30 seconds | Max time to establish WebSocket |
| Authentication | 30 seconds | Time to authenticate after connect |
| Request | 30 seconds | Max time for request response |
| Idle | 5 minutes | Connection closed if no activity |
| Session | 24 hours | Maximum connection duration |
Request Timeouts
Implement timeouts for all requests:
class RequestManager {
constructor(ws) {
this.ws = ws;
this.requests = new Map();
this.defaultTimeout = 30000; // 30 seconds
}
sendRequest(request, timeout = this.defaultTimeout) {
return new Promise((resolve, reject) => {
const requestId = request.requestId;
// Store request handler
this.requests.set(requestId, {
resolve,
reject,
timestamp: Date.now()
});
// Set timeout
const timeoutId = setTimeout(() => {
const handler = this.requests.get(requestId);
if (handler) {
this.requests.delete(requestId);
handler.reject(new Error(`Request timeout: ${requestId}`));
}
}, timeout);
// Store timeout ID for cleanup
this.requests.get(requestId).timeoutId = timeoutId;
// Send request
this.ws.send(JSON.stringify(request));
});
}
handleResponse(response) {
const handler = this.requests.get(response.requestId);
if (handler) {
clearTimeout(handler.timeoutId);
this.requests.delete(response.requestId);
if (response.error) {
handler.reject(new Error(response.error.message));
} else {
handler.resolve(response.result);
}
}
}
cleanup() {
// Clear all pending requests
this.requests.forEach((handler, requestId) => {
clearTimeout(handler.timeoutId);
handler.reject(new Error('Connection closed'));
});
this.requests.clear();
}
}Connection Management
Complete Connection Manager
class WebSocketConnection {
constructor(url) {
this.url = url;
this.ws = null;
this.heartbeat = null;
this.requestManager = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.sessionStartTime = null;
this.isAuthenticated = false;
}
async connect() {
return new Promise((resolve, reject) => {
// Connection timeout
const connectTimeout = setTimeout(() => {
reject(new Error('Connection timeout'));
this.ws?.close();
}, 30000);
this.ws = new WebSocket(this.url);
this.sessionStartTime = Date.now();
this.ws.onopen = () => {
clearTimeout(connectTimeout);
console.log('WebSocket connected');
// Initialize managers
this.heartbeat = new HeartbeatManager(this.ws);
this.requestManager = new RequestManager(this.ws);
// Start heartbeat
this.heartbeat.start();
// Start session timer
this.startSessionTimer();
resolve();
};
this.ws.onclose = (event) => {
this.handleClose(event);
};
this.ws.onerror = (error) => {
clearTimeout(connectTimeout);
this.handleError(error);
reject(error);
};
this.ws.onmessage = (event) => {
this.handleMessage(event);
};
});
}
handleMessage(event) {
try {
const message = JSON.parse(event.data);
// Route to appropriate handler
if (message.type === 'response') {
this.requestManager.handleResponse(message);
} else if (message.type === 'pong') {
// Handled by heartbeat manager
} else if (message.type === 'notification') {
this.handleNotification(message);
}
} catch (error) {
console.error('Message parse error:', error);
}
}
handleClose(event) {
console.log(`WebSocket closed: ${event.code} - ${event.reason}`);
// Cleanup
this.heartbeat?.stop();
this.requestManager?.cleanup();
this.isAuthenticated = false;
// Determine if should reconnect
if (event.code !== 1000 && this.shouldReconnect()) {
this.scheduleReconnect();
}
}
handleError(error) {
console.error('WebSocket error:', error);
}
shouldReconnect() {
// Don't reconnect if session expired
if (this.isSessionExpired()) {
console.log('Session expired - not reconnecting');
return false;
}
// Check reconnect attempts
return this.reconnectAttempts < this.maxReconnectAttempts;
}
scheduleReconnect() {
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000 // Max 30 seconds
);
this.reconnectAttempts++;
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(async () => {
try {
await this.connect();
this.reconnectAttempts = 0; // Reset on success
// Re-authenticate if needed
if (this.url.includes('/trade')) {
await this.authenticate();
}
} catch (error) {
console.error('Reconnection failed:', error);
}
}, delay);
}
startSessionTimer() {
// Force disconnect after 24 hours
const sessionTimeout = 24 * 60 * 60 * 1000; // 24 hours
setTimeout(() => {
console.log('Session expired - closing connection');
this.ws.close(1000, 'Session expired');
}, sessionTimeout);
}
isSessionExpired() {
if (!this.sessionStartTime) return false;
const sessionDuration = Date.now() - this.sessionStartTime;
return sessionDuration >= 24 * 60 * 60 * 1000;
}
async authenticate() {
// Implement authentication with timeout
const authTimeout = 30000; // 30 seconds
try {
const authRequest = {
type: 'auth',
requestId: 'auth-1',
params: {
// Auth params
}
};
const result = await this.requestManager.sendRequest(authRequest, authTimeout);
if (result.authenticated) {
this.isAuthenticated = true;
console.log('Authenticated successfully');
} else {
throw new Error('Authentication failed');
}
} catch (error) {
console.error('Authentication error:', error);
this.ws.close(1008, 'Authentication failed');
throw error;
}
}
close() {
this.heartbeat?.stop();
this.requestManager?.cleanup();
this.ws?.close(1000, 'Normal closure');
}
getConnectionStats() {
return {
readyState: this.ws?.readyState,
isAuthenticated: this.isAuthenticated,
sessionAge: Date.now() - this.sessionStartTime,
lastPongAge: this.heartbeat?.getLastPongAge(),
reconnectAttempts: this.reconnectAttempts,
pendingRequests: this.requestManager?.requests.size || 0
};
}
}Monitoring Connection Health
class ConnectionMonitor {
constructor(connection) {
this.connection = connection;
this.interval = null;
}
start() {
this.interval = setInterval(() => {
const stats = this.connection.getConnectionStats();
// Check connection health
if (stats.readyState !== WebSocket.OPEN) {
console.warn('Connection not open:', stats.readyState);
}
// Check heartbeat
if (stats.lastPongAge > 60000) {
console.error('No pong received for 60s - connection may be dead');
}
// Check session age
const hoursConnected = stats.sessionAge / (1000 * 60 * 60);
if (hoursConnected > 23) {
console.warn(`Session approaching 24h limit: ${hoursConnected.toFixed(1)}h`);
}
// Log stats
console.log('Connection stats:', {
...stats,
sessionAge: `${(stats.sessionAge / 1000).toFixed(0)}s`,
lastPongAge: `${(stats.lastPongAge / 1000).toFixed(0)}s`
});
}, 30000); // Check every 30 seconds
}
stop() {
clearInterval(this.interval);
}
}Implementation Notes
Heartbeat Implementation
Application-level heartbeats enable connection monitoring:
// Start heartbeat immediately after connection
ws.onopen = () => {
heartbeat.start();
};2. Handle All Timeout Scenarios
const timeouts = {
connection: 30000,
authentication: 30000,
request: 30000,
heartbeat: 30000,
pong: 10000
};3. Graceful Shutdown
async function shutdown(connection) {
console.log('Shutting down gracefully...');
// Cancel pending requests
connection.requestManager.cleanup();
// Stop heartbeat
connection.heartbeat.stop();
// Close connection
connection.close();
console.log('Shutdown complete');
}4. Monitor Connection Quality
Track metrics to identify issues:
- Round-trip time (RTT)
- Missed pongs
- Reconnection frequency
- Request timeout rate
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Frequent disconnects | Network instability | Implement robust reconnection |
| Pong timeouts | High latency | Increase pong timeout |
| Session expiry | 24-hour limit | Plan for reconnection |
| Stuck requests | No timeout | Request timeout configuration required |
Next Steps
- WebSocket Overview - WebSocket basics
- Trade WebSocket - Trading implementation
- Error Handling - Timeout error patterns