Skip to main content

WebSocket Overview

The router proxies WebSocket connections to backend Solana RPC nodes, enabling subscription methods like accountSubscribe, logsSubscribe, and slotSubscribe. WebSocket connections use the same authentication, rate limiting, and weighted load balancing as HTTP requests.

Connection Methods

Clients can connect via two equivalent endpoints:
  1. Main HTTP Port: GET / with Upgrade: websocket header
  2. Dedicated WebSocket Port: HTTP port + 1 (e.g., if HTTP is 28899, WebSocket is 28900)

GET / (WebSocket Upgrade)

Upgrade an HTTP connection to WebSocket and proxy to a backend Solana RPC node.

Connection URL

ws://host:port/?api-key=YOUR_API_KEY
or
ws://host:port+1/?api-key=YOUR_API_KEY

Authentication

WebSocket connections require an API key passed as a query parameter:
ws://rpc.example.com:28899/?api-key=YOUR_API_KEY
Authentication is validated before the upgrade completes. Failed auth returns standard HTTP error responses:
  • 401 Unauthorized: Invalid or missing API key
  • 429 Too Many Requests: Rate limit exceeded
  • 500 Internal Server Error: Redis connection error
  • 503 Service Unavailable: No healthy WebSocket backends available

Backend Selection

The router selects backends with WebSocket support (ws_url configured) using weighted random selection among healthy backends. Backends without ws_url are excluded from WebSocket routing. Configuration:
[[backends]]
label  = "mainnet-primary"
url    = "https://api.mainnet-beta.solana.com"      # HTTP URL
ws_url = "wss://api.mainnet-beta.solana.com"       # WebSocket URL (required)
weight = 10

[[backends]]
label  = "http-only-backend"
url    = "https://rpc.example.com"
weight = 5
# No ws_url - excluded from WebSocket routing

Connection Lifecycle

Message Forwarding

All WebSocket frames are relayed transparently between client and backend:
  • Text: JSON-RPC subscription requests/responses/notifications
  • Binary: Raw binary data (if used by backend)
  • Ping/Pong: Keepalive frames (forwarded bidirectionally)
  • Close: Graceful shutdown signal
When either side closes the connection or errors, the router shuts down both directions.

Supported Subscription Methods

The router transparently proxies all Solana WebSocket subscription methods:
accountSubscribe
subscription
Subscribe to account data changes
logsSubscribe
subscription
Subscribe to transaction logs
programSubscribe
subscription
Subscribe to program account changes
signatureSubscribe
subscription
Subscribe to transaction confirmation
slotSubscribe
subscription
Subscribe to slot changes
slotsUpdatesSubscribe
subscription
Subscribe to slot update notifications
rootSubscribe
subscription
Subscribe to root changes
voteSubscribe
subscription
Subscribe to vote transactions

Connection Examples

const WebSocket = require('ws');

const ws = new WebSocket('ws://rpc.example.com:28899/?api-key=YOUR_API_KEY');

ws.on('open', () => {
  console.log('Connected');
  
  // Subscribe to account changes
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'accountSubscribe',
    params: [
      'vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg',
      { encoding: 'jsonParsed' }
    ]
  }));
});

ws.on('message', (data) => {
  const msg = JSON.parse(data);
  console.log('Received:', msg);
  
  if (msg.method === 'accountNotification') {
    console.log('Account updated:', msg.params);
  }
});

ws.on('error', (err) => {
  console.error('WebSocket error:', err);
});

ws.on('close', () => {
  console.log('Disconnected');
});

Error Responses

401 Unauthorized
string
Returned before upgrade when:
  • No api-key query parameter provided
  • Invalid API key
  • API key is inactive
Response body: "Unauthorized"
429 Too Many Requests
string
Returned when the API key exceeds its rate limit during connection attempt.Response body: "Rate limit exceeded"
500 Internal Server Error
string
Returned when Redis connection fails during key validation.Response body: "Internal Server Error"
503 Service Unavailable
string
Returned when no healthy backends with ws_url configured are available.Response body: "No healthy WebSocket backends available"

Rate Limiting

WebSocket connections are rate limited at connection time using the same Redis-backed mechanism as HTTP requests:
  1. API key is validated and rate limit checked before the upgrade
  2. If rate limit is exceeded, connection is rejected with 429 Too Many Requests
  3. After successful upgrade, messages are not individually rate limited
  4. Long-lived connections do not count against the per-second rate limit

Metrics

WebSocket connections emit the following metrics (see Metrics Endpoint for details):
  • ws_connections_total: Counter of connection attempts by status
  • ws_active_connections: Gauge of currently open connections
  • ws_messages_total: Counter of messages relayed (client→backend, backend→client)
  • ws_connection_duration_seconds: Histogram of connection lifetimes
All metrics include backend and owner labels for granular analysis.

Monitoring Active Connections

Query Active Connections
curl -s http://localhost:9090/metrics | grep ws_active_connections

# Output:
ws_active_connections{backend="mainnet-primary",owner="my-client"} 12
ws_active_connections{backend="backup-rpc",owner="other-client"} 5

Connection Timeout

WebSocket connections do not have an idle timeout. Connections remain open until:
  • Client sends a Close frame
  • Backend sends a Close frame
  • Network error or connection loss
  • Router shutdown
Clients should implement reconnection logic with exponential backoff for production use.

Best Practices

  1. Reconnection Logic: Implement automatic reconnection with exponential backoff
  2. Heartbeat: Use Ping/Pong frames to detect stale connections
  3. Error Handling: Handle backend disconnects and router restarts gracefully
  4. Subscription Management: Track subscription IDs and resubscribe on reconnect
  5. Rate Limit Headroom: Ensure API key rate limits account for subscription setup traffic

Example: Reconnection with Backoff

Reconnection Logic
class ResilientWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000;
    this.maxReconnectDelay = 30000;
    this.connect();
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.on('open', () => {
      console.log('Connected');
      this.reconnectDelay = 1000; // Reset backoff
      this.resubscribe();
    });
    
    this.ws.on('close', () => {
      console.log(`Disconnected, reconnecting in ${this.reconnectDelay}ms`);
      setTimeout(() => this.connect(), this.reconnectDelay);
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
    });
    
    this.ws.on('error', (err) => {
      console.error('WebSocket error:', err);
      this.ws.close();
    });
  }
  
  resubscribe() {
    // Re-establish subscriptions after reconnect
    this.ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'slotSubscribe'
    }));
  }
}

const ws = new ResilientWebSocket('ws://rpc.example.com:28899/?api-key=YOUR_API_KEY');

Build docs developers (and LLMs) love