Connection & Errors

Connection Management

Connection Lifecycle

Important: WebSocket client sessions have a maximum duration of 1 hour. For long-running applications, you must implement reconnection logic to maintain persistent data streams. There is no WebSocket-level ping/pong keepalive mechanism built into the server. It is your responsibility as the client to handle reconnection for long-living sessions.

The REST POST /heartbeat endpoint is a separate feature for automatic order cancellation (dead man's switch) and does not affect WebSocket connection lifecycle.

Error Handling

Error responses follow this format:

{
  "kind": "response",
  "type": "error",
  "id": "optional-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": "Description of the error"
}

Common error scenarios:

  • Authentication failures:

    • "Failed to validate api key" - Generic validation failure
    • "Not Authorized" - Not authenticated
    • "Session already authorized" - Already authenticated
    • Specific codes: NOT_FOUND, FORBIDDEN, KEY_USAGE_EXCEEDED, RATELIMITED
    • See the API Key Guide for troubleshooting.
  • Invalid subscriptions:

    • "Invalid subscription parameters"
    • "Invalid query for [CHANNEL] channel"
  • Invalid messages:

    • "invalid [MESSAGE_TYPE] payload [error_details]"

Error Handling Example

function handleWebSocketError(error: { type: string; success: boolean; error: string }) {
  if (
    error.error.includes('Failed to validate api key') ||
    error.error === 'NOT_FOUND' ||
    error.error === 'FORBIDDEN' ||
    error.error === 'KEY_USAGE_EXCEEDED'
  ) {
    console.error('API key error:', error.error);
  } else if (error.error === 'RATELIMITED') {
    console.warn('Rate limited. Implementing backoff...');
  } else if (error.error === 'Not Authorized') {
    console.warn('Not authenticated. Attempting to authenticate...');
  } else if (error.error === 'Session already authorized') {
    console.info('Session already authenticated');
  } else if (error.error.includes('Invalid subscription parameters')) {
    console.error('Invalid subscription:', error.error);
  } else {
    console.error('Unhandled WebSocket error:', error.error);
  }
}

Rate Limiting

The WebSocket API implements per-session rate limiting to ensure fair usage and system stability.

Rate Limits

Message TypeRate LimitWindowScope
auth1 request1 secondPer client IP address
get_instruments1 request1 secondPer API key owner
get_subscriptions1 request1 secondPer API key owner
logout1 request1 secondPer API key owner
subscribe10 requests1 secondPer API key owner
unsubscribe10 requests1 secondPer API key owner
unsubscribe_all1 request1 secondPer API key owner
get_ob_state_by_instruments5 requests20 secondsPer API key owner
get_ob_state_by_market5 requests20 secondsPer API key owner
resend5 requests10 secondsPer API key owner

Rate limits use a token bucket algorithm. All sessions for the same API key owner share rate limits. The auth request is rate-limited per client IP address before API key verification.

Rate Limit Response

{
  "kind": "response",
  "type": "error",
  "id": "your-request-id",
  "timestamp_ms": 1677721600000,
  "success": false,
  "error": {
    "type": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded for endpoint 'get_ob_state_by_instruments'. Limit: 5 requests per 20 seconds.",
    "data": {
      "remainingRequests": 0,
      "resetTime": 1677721623000
    }
  }
}
FieldDescription
error.type"RATE_LIMIT_EXCEEDED"
error.messageHuman-readable description
error.data.remainingRequestsRemaining requests in current window
error.data.resetTimeUnix timestamp (ms) when the rate limit resets

Rate Limit Handler

function handleRateLimitedResponse(response: any) {
  if (!response.success && response.error?.type === 'RATE_LIMIT_EXCEEDED') {
    const resetTime = response.error.data?.resetTime;
    const now = Date.now();
    const waitMs = resetTime ? Math.max(0, resetTime - now) : 20000;

    console.warn(`Rate limited. Waiting ${Math.ceil(waitMs / 1000)} seconds...`);

    setTimeout(() => {
      console.log('Rate limit window reset. Safe to retry.');
    }, waitMs);
  }
}

Instead of polling with get_ob_state_*, use channel subscriptions for real-time updates (no rate limit on event delivery).

Reconnection Strategy

class WebSocketClient {
  private ws: WebSocket | null = null;
  private readonly url: string;
  private readonly apiKey: string;
  private reconnectAttempts: number = 0;
  private maxReconnectAttempts: number = 5;
  private reconnectInterval: number = 1000;
  private subscriptions: Array<{
    channel: string;
    query: Record<string, any>;
  }> = [];
  private isAuthenticated: boolean = false;

  constructor(url: string, apiKey: string) {
    this.url = url;
    this.apiKey = apiKey;
  }

  connect(): void {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('Connected to WebSocket');
      this.reconnectAttempts = 0;
      this.reconnectInterval = 1000;
      this.authenticate();
    };

    this.ws.onclose = (event) => {
      console.log(`Connection closed: ${event.code} - ${event.reason}`);
      this.isAuthenticated = false;

      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        console.log(
          `Reconnecting in ${this.reconnectInterval}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`
        );

        setTimeout(() => {
          this.reconnectAttempts++;
          this.reconnectInterval = Math.min(this.reconnectInterval * 2, 30000);
          this.connect();
        }, this.reconnectInterval);
      } else {
        console.error('Max reconnection attempts reached.');
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleMessage(message);
    };
  }

  private authenticate(): void {
    if (!this.ws) return;
    this.ws.send(JSON.stringify({ type: 'auth', api_key: this.apiKey }));
  }

  private handleMessage(message: any): void {
    if (message.type === 'auth' && message.success) {
      console.log('Authenticated successfully');
      this.isAuthenticated = true;
      if (this.subscriptions.length > 0) {
        this.resubscribe();
      }
    } else if (message.type === 'auth' && !message.success) {
      console.error('Authentication failed:', message.error);
    } else if (message.event) {
      console.log(`Event received: ${message.event}`, message.data);
    }
  }

  subscribe(
    subscriptions: Array<{ channel: string; query: Record<string, any> }>
  ): void {
    if (!this.isAuthenticated) {
      console.warn('Cannot subscribe: not authenticated');
      return;
    }
    this.subscriptions = [...this.subscriptions, ...subscriptions];
    this.ws?.send(JSON.stringify({ type: 'subscribe', subscriptions }));
  }

  private resubscribe(): void {
    if (this.subscriptions.length > 0) {
      console.log('Resubscribing after reconnection...');
      this.ws?.send(JSON.stringify({
        type: 'subscribe',
        subscriptions: this.subscriptions
      }));
    }
  }

  disconnect(): void {
    if (this.ws) {
      this.reconnectAttempts = this.maxReconnectAttempts;
      this.ws.send(JSON.stringify({ type: 'logout' }));
      this.ws.close();
      this.ws = null;
      this.isAuthenticated = false;
    }
  }

  forceReconnect(): void {
    this.reconnectAttempts = 0;
    this.reconnectInterval = 1000;
    if (this.ws) {
      this.ws.close();
    }
    this.connect();
  }
}

// Usage
const client = new WebSocketClient('wss://staging.kyan.sh/ws', 'your-api-key');
client.connect();

setTimeout(() => {
  client.subscribe([
    { channel: 'index_price', query: { pair: 'BTC_USDC' } }
  ]);
}, 2000);

Best Practices

  1. Connection Management:

    • Always authenticate immediately after connecting
    • Implement robust reconnection logic with exponential backoff - connections timeout after 1 hour
    • Handle connection drops gracefully and automatically
    • Store subscription state for seamless reconnection
    • Monitor connection health and implement manual reconnection triggers
  2. Subscription Management:

    • Keep track of active subscriptions for reconnection
    • Automatically resubscribe after successful reconnection
    • Unsubscribe from channels you no longer need to reduce bandwidth
    • Validate subscription success before considering them active
  3. Error Handling:

    • Implement comprehensive error handling for all message types
    • Log errors for debugging and monitoring
    • Handle rate limiting with exponential backoff
    • Differentiate between recoverable and non-recoverable errors
  4. Performance:

    • Batch subscriptions when possible to reduce overhead
    • Process messages efficiently to avoid blocking
    • Consider using worker threads for heavy message processing
    • Implement message queuing during reconnection periods
  5. Security:

    • Never expose API keys in client-side code
    • Use secure WebSocket connections (wss://) only
    • Validate all incoming data before processing
    • Implement proper authentication state management
  6. Reliability:

    • Plan for regular disconnections - client sessions expire after 1 hour
    • Implement connection health monitoring and automatic reconnection
    • Use proper logging to track connection lifecycle events
    • Test reconnection logic thoroughly in development