Intermediate10 min read
Edit on GitHub

WebSocket Connections

Use WebSocket for efficient, persistent API connections

WebSocket connections provide significant advantages over HTTP for Cardano API access. This guide covers connection management, best practices, and common patterns.

Choose Your Network

NetworkWebSocket Endpoint
Mainnetwss://api.nacho.builders/v1/ogmios
Preprodwss://api.nacho.builders/v1/preprod/ogmios

Your API key works on both networks. All examples below use Mainnet - add /preprod to the path for testnet.

Why WebSocket?

FeatureHTTPWebSocket
Connection overheadPer requestOnce
LatencyHigherLower
Chain syncNot supportedSupported
PipeliningNoYes
Real-time updatesPollingPush

Use WebSocket when you need:

  • Chain synchronization
  • Multiple queries in sequence
  • Real-time block monitoring
  • Lower latency responses

Basic Connection

const API_KEY = process.env.NACHO_API_KEY;
const ws = new WebSocket(`wss://api.nacho.builders/v1/ogmios?apikey=${API_KEY}`);

ws.onopen = () => {
console.log('Connected!');

// Send a query
ws.send(JSON.stringify({
  jsonrpc: '2.0',
  method: 'queryNetwork/tip',
  id: 'tip-query'
}));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Response:', data);

if (data.id === 'tip-query') {
  console.log('Current tip:', data.result);
}
};

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

ws.onclose = (event) => {
console.log('Connection closed:', event.code, event.reason);
};

Connection Lifecycle

1. Connection States

ws.readyState === WebSocket.CONNECTING  // 0
ws.readyState === WebSocket.OPEN        // 1
ws.readyState === WebSocket.CLOSING     // 2
ws.readyState === WebSocket.CLOSED      // 3

2. Heartbeat / Keep-Alive

Connections may timeout after inactivity. Send periodic pings:

class WebSocketManager {
  constructor(url) {
    this.url = url;
    this.pingInterval = null;  // Will hold the interval timer
    this.connect();
  }

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

    this.ws.onopen = () => {
      // Start heartbeat to keep connection alive
      // Without this, idle connections may be closed by the server
      this.pingInterval = setInterval(() => {
        // Only send if connection is still open
        if (this.ws.readyState === WebSocket.OPEN) {
          // Use a lightweight query as a "ping"
          this.ws.send(JSON.stringify({
            jsonrpc: '2.0',
            method: 'queryNetwork/tip',
            id: 'heartbeat'  // ID helps identify heartbeat responses
          }));
        }
      }, 30000); // Send every 30 seconds
    };

    this.ws.onclose = () => {
      // Stop heartbeat when connection closes
      clearInterval(this.pingInterval);
      // Add reconnect logic here (see Reconnection Strategy section)
    };
  }
}

3. Graceful Shutdown

function shutdown() {
  // Stop accepting new requests
  accepting = false;

  // Wait for pending requests
  await Promise.all(pendingRequests);

  // Close connection with normal closure code
  ws.close(1000, 'Shutting down');
}

Request Pipelining

Send multiple requests without waiting for responses:

// Pipeline multiple queries
const queries = [
  { method: 'queryLedgerState/epoch', id: 'epoch' },
  { method: 'queryNetwork/tip', id: 'tip' },
  { method: 'queryLedgerState/protocolParameters', id: 'params' }
];

const results = new Map();

ws.onopen = () => {
  queries.forEach(q => {
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      method: q.method,
      id: q.id
    }));
  });
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  results.set(data.id, data.result);

  if (results.size === queries.length) {
    console.log('All results received:', Object.fromEntries(results));
  }
};

Error Handling

Connection Errors

ws.onerror = (event) => {
  console.error('WebSocket error:', event);
  // Connection will close after error
};

ws.onclose = (event) => {
  switch (event.code) {
    case 1000:
      console.log('Normal closure');
      break;
    case 1001:
      console.log('Going away (server shutdown)');
      break;
    case 1006:
      console.log('Abnormal closure (network issue)');
      break;
    case 1008:
      console.log('Policy violation (invalid API key)');
      break;
    default:
      console.log('Closed with code:', event.code);
  }
};

Protocol Errors

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.error) {
    console.error(`Error [${data.error.code}]: ${data.error.message}`);

    // Handle specific errors
    if (data.error.code === -32601) {
      console.error('Method not found');
    }
  }
};

Reconnection Strategy

/**
 * WebSocket wrapper with automatic reconnection using exponential backoff.
 * Handles temporary network issues gracefully.
 */
class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.maxRetries = options.maxRetries || 10;   // Give up after this many attempts
    this.retryDelay = options.retryDelay || 1000; // Base delay in milliseconds
    this.retries = 0;
    this.connect();
  }

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

    this.ws.onopen = () => {
      console.log('Connected');
      this.retries = 0; // Reset retry counter on successful connection
    };

    this.ws.onclose = (event) => {
      // Only reconnect if:
      // 1. It wasn't a normal closure (code 1000)
      // 2. We haven't exceeded max retries
      if (event.code !== 1000 && this.retries < this.maxRetries) {
        // Exponential backoff: 1s, 2s, 4s, 8s, etc.
        // Prevents overwhelming the server during outages
        const delay = this.retryDelay * Math.pow(2, this.retries);
        console.log(`Reconnecting in ${delay}ms...`);

        setTimeout(() => {
          this.retries++;
          this.connect();  // Attempt reconnection
        }, delay);
      }
    };

    this.ws.onerror = () => {
      // Errors trigger onclose, which handles reconnection
      // No need to duplicate logic here
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      // Connection not ready - queue message for later
      // (requires implementing a message queue)
      this.queue.push(data);
    }
  }
}

Connection Limits

TierMax Connections
FREE5 concurrent
PAID50 concurrent

Exceeding connection limits will result in new connections being rejected. Close unused connections promptly.

Billing & Rate Limits

WebSocket connections are billed per message in both directions (sent and received). This matches industry standards used by providers like QuickNode and Alchemy.

How Messages Are Counted

DirectionDescriptionCounted
SentMessages you send to the API (queries, nextBlock, etc.)Yes
ReceivedResponses from the API (results, blocks, rollbacks)Yes

Credit Usage

TierCredits per Message
FREE0 (uses daily limits)
PAID1 credit per message

For example, a chain sync session receiving 100 blocks would consume approximately:

  • ~100 nextBlock requests (sent)
  • ~100 block responses (received)
  • = ~200 credits for PAID tier

Rate Limits

Rate limits apply to WebSocket messages, not just HTTP requests:

TierRate Limit
FREE100 msg/sec
PAID500 msg/sec

When you exceed the rate limit, you'll receive an error response:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32029,
    "message": "Rate limit exceeded. Please slow down.",
    "data": { "retryAfter": 1000, "remaining": 0, "limit": 100 }
  },
  "id": "your-request-id"
}

For chain synchronization, normal operation (following the tip) uses only ~3-6 messages per minute. Rate limits primarily affect historical sync or high-frequency querying.

Best Practices

  1. Reuse connections - Don't create new connections for each query
  2. Implement heartbeats - Keep connections alive during idle periods
  3. Handle reconnection - Network issues will happen
  4. Use request IDs - Match responses to requests in pipelined scenarios
  5. Close gracefully - Use code 1000 for normal closure
  6. Monitor connection state - Check readyState before sending

Connection Pool Example

For high-throughput applications:

/**
 * Pool of WebSocket connections for high-throughput scenarios.
 * Distributes load across multiple connections using round-robin.
 */
class ConnectionPool {
  constructor(url, size = 5) {
    this.connections = [];
    this.index = 0;  // Current position in round-robin rotation

    // Create multiple connections upfront
    for (let i = 0; i < size; i++) {
      this.connections.push(new ReconnectingWebSocket(url));
    }
  }

  getConnection() {
    // Round-robin: cycle through connections evenly
    // This distributes load and prevents any single connection from bottlenecking
    const conn = this.connections[this.index];
    this.index = (this.index + 1) % this.connections.length;
    return conn;
  }

  async query(method, params) {
    // Get next connection in rotation and send query
    const conn = this.getConnection();
    return conn.query(method, params);
  }

  closeAll() {
    // Clean shutdown of all connections
    this.connections.forEach(c => c.close());
  }
}

Was this page helpful?