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
| Network | WebSocket Endpoint |
|---|---|
| Mainnet | wss://api.nacho.builders/v1/ogmios |
| Preprod | wss://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?
| Feature | HTTP | WebSocket |
|---|---|---|
| Connection overhead | Per request | Once |
| Latency | Higher | Lower |
| Chain sync | Not supported | Supported |
| Pipelining | No | Yes |
| Real-time updates | Polling | Push |
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 // 32. 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
| Tier | Max Connections |
|---|---|
| FREE | 5 concurrent |
| PAID | 50 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
| Direction | Description | Counted |
|---|---|---|
| Sent | Messages you send to the API (queries, nextBlock, etc.) | Yes |
| Received | Responses from the API (results, blocks, rollbacks) | Yes |
Credit Usage
| Tier | Credits per Message |
|---|---|
| FREE | 0 (uses daily limits) |
| PAID | 1 credit per message |
For example, a chain sync session receiving 100 blocks would consume approximately:
- ~100
nextBlockrequests (sent) - ~100 block responses (received)
- = ~200 credits for PAID tier
Rate Limits
Rate limits apply to WebSocket messages, not just HTTP requests:
| Tier | Rate Limit |
|---|---|
| FREE | 100 msg/sec |
| PAID | 500 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
- Reuse connections - Don't create new connections for each query
- Implement heartbeats - Keep connections alive during idle periods
- Handle reconnection - Network issues will happen
- Use request IDs - Match responses to requests in pipelined scenarios
- Close gracefully - Use code 1000 for normal closure
- 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?