FortiBlox LogoFortiBlox Docs
NexusWebSocket Streaming

WebSocket Best Practices

Optimize your WebSocket implementation for reliability and performance

WebSocket Best Practices

Follow these guidelines to build robust, efficient WebSocket applications with FortiBlox.

Broadcast Mode

WebSocket currently broadcasts all network transactions. These best practices focus on efficiently handling high-volume streams and implementing client-side filtering.

Connection Management

Implement Reconnection Logic

Always handle connection drops with exponential backoff:

class WebSocketClient {
  constructor(url, apiKey) {
    this.url = url;
    this.apiKey = apiKey;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.baseDelay = 1000;
  }

  connect() {
    this.ws = new WebSocket(this.url, {
      headers: { 'X-API-Key': this.apiKey }
    });

    this.ws.on('open', () => {
      console.log('Connected');
      this.reconnectAttempts = 0;
      this.resubscribe();
    });

    this.ws.on('close', () => {
      this.reconnect();
    });

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

  reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.reconnectAttempts++;
    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.reconnectAttempts),
      30000 // Max 30 seconds
    );

    console.log(`Reconnecting in ${delay}ms...`);
    setTimeout(() => this.connect(), delay);
  }

  resubscribe() {
    // Re-establish your subscriptions after reconnection
    this.subscriptions.forEach(sub => {
      this.ws.send(JSON.stringify(sub));
    });
  }
}

Monitor Connection Health

Detect stale connections with heartbeat monitoring:

class HealthyWebSocket {
  constructor(url, apiKey) {
    this.lastMessageTime = Date.now();
    this.healthCheckInterval = 30000; // 30 seconds
    this.staleThreshold = 60000; // 60 seconds

    this.connect(url, apiKey);
    this.startHealthCheck();
  }

  connect(url, apiKey) {
    this.ws = new WebSocket(url, {
      headers: { 'X-API-Key': apiKey }
    });

    this.ws.on('message', (data) => {
      this.lastMessageTime = Date.now();
      this.handleMessage(data);
    });

    // Implement ping/pong
    this.ws.on('ping', () => {
      this.ws.pong();
      this.lastMessageTime = Date.now();
    });
  }

  startHealthCheck() {
    this.healthCheckTimer = setInterval(() => {
      const timeSinceLastMessage = Date.now() - this.lastMessageTime;

      if (timeSinceLastMessage > this.staleThreshold) {
        console.warn('Connection appears stale, reconnecting...');
        this.ws.close();
      }
    }, this.healthCheckInterval);
  }

  cleanup() {
    clearInterval(this.healthCheckTimer);
    this.ws.close();
  }
}

Client-Side Filtering

Since WebSocket broadcasts all transactions, implement efficient client-side filtering:

Use Set for Fast Lookups

// ✅ Good: Use Set for O(1) lookups
const WATCHED_ACCOUNTS = new Set([
  'Account1Address',
  'Account2Address',
  'Account3Address'
]);

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message.type === 'transaction') {
    const hasWatchedAccount = message.data.accounts?.some(
      account => WATCHED_ACCOUNTS.has(account)
    );

    if (hasWatchedAccount) {
      processTransaction(message.data);
    }
  }
});
// ❌ Bad: Array includes() is O(n)
const watchedAccounts = ['Account1', 'Account2', 'Account3'];

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message.type === 'transaction') {
    const hasWatchedAccount = message.data.accounts?.some(
      account => watchedAccounts.includes(account)
    );

    if (hasWatchedAccount) {
      processTransaction(message.data);
    }
  }
});

Filter Multiple Criteria

class TransactionFilter {
  constructor(options = {}) {
    this.watchedAccounts = new Set(options.accounts || []);
    this.watchedPrograms = new Set(options.programs || []);
    this.onlySuccessful = options.onlySuccessful || false;
  }

  matches(txData) {
    // Filter by success/failure
    if (this.onlySuccessful && txData.err !== null) {
      return false;
    }

    // Filter by accounts
    if (this.watchedAccounts.size > 0) {
      const hasAccount = txData.accounts?.some(
        account => this.watchedAccounts.has(account)
      );
      if (!hasAccount) return false;
    }

    // Filter by programs (check accountKeys if available)
    if (this.watchedPrograms.size > 0) {
      const hasProgram = txData.accountKeys?.some(
        key => this.watchedPrograms.has(key)
      );
      if (!hasProgram) return false;
    }

    return true;
  }
}

// Usage
const filter = new TransactionFilter({
  accounts: ['MyWallet1', 'MyWallet2'],
  programs: ['DEXProgramAddress'],
  onlySuccessful: true
});

ws.on('message', (data) => {
  const message = JSON.parse(data);

  if (message.type === 'transaction' && filter.matches(message.data)) {
    processTransaction(message.data);
  }
});

Need Server-Side Filtering?

For server-side filtering by account/program, use the Geyser REST API instead of WebSocket. It's more efficient for specific queries.

Batch Processing

Process multiple messages efficiently:

class BatchProcessor {
  constructor(batchSize = 100, flushInterval = 1000) {
    this.batch = [];
    this.batchSize = batchSize;
    this.flushInterval = flushInterval;

    setInterval(() => this.flush(), this.flushInterval);
  }

  addMessage(message) {
    this.batch.push(message);

    if (this.batch.length >= this.batchSize) {
      this.flush();
    }
  }

  flush() {
    if (this.batch.length === 0) return;

    console.log(`Processing ${this.batch.length} messages`);
    // Process batch
    this.processBatch(this.batch);

    this.batch = [];
  }

  processBatch(messages) {
    // Your batch processing logic
  }
}

const processor = new BatchProcessor();

ws.on('message', (data) => {
  const message = JSON.parse(data);
  processor.addMessage(message);
});

Error Handling

Graceful Degradation

Handle errors without crashing your application:

ws.on('message', (data) => {
  try {
    const message = JSON.parse(data);
    processMessage(message);
  } catch (error) {
    console.error('Failed to process message:', error);
    // Log to error tracking service
    logError(error, { data });
    // Continue processing other messages
  }
});

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

  // Categorize errors
  if (error.code === 'ECONNREFUSED') {
    console.log('Connection refused, check network');
  } else if (error.code === 'ETIMEDOUT') {
    console.log('Connection timeout, retrying...');
  }

  // Don't let errors crash the application
});

Rate Limit Handling

Respect rate limits and implement backoff:

ws.on('close', (code, reason) => {
  if (code === 429) {
    console.log('Rate limit exceeded');
    // Longer backoff for rate limits
    const delay = 60000; // 1 minute
    setTimeout(() => this.reconnect(), delay);
  } else {
    this.reconnect();
  }
});

Performance Optimization

Connection Pooling

Reuse connections for multiple subscriptions:

class WebSocketPool {
  constructor(maxConnections = 5) {
    this.connections = [];
    this.maxConnections = maxConnections;
    this.currentIndex = 0;
  }

  getConnection() {
    if (this.connections.length < this.maxConnections) {
      const ws = this.createConnection();
      this.connections.push(ws);
      return ws;
    }

    // Round-robin existing connections
    const ws = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return ws;
  }

  createConnection() {
    return new WebSocket(url, {
      headers: { 'X-API-Key': apiKey }
    });
  }
}

Memory Management

Prevent memory leaks by cleaning up properly:

class ManagedWebSocket {
  constructor() {
    this.messageHandlers = new Set();
    this.connect();
  }

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

    this.ws.on('message', (data) => {
      const message = JSON.parse(data);

      // Call all registered handlers
      this.messageHandlers.forEach(handler => {
        handler(message);
      });
    });
  }

  addHandler(handler) {
    this.messageHandlers.add(handler);

    // Return cleanup function
    return () => {
      this.messageHandlers.delete(handler);
    };
  }

  disconnect() {
    // Clean up all handlers
    this.messageHandlers.clear();

    // Close connection
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// Usage
const ws = new ManagedWebSocket();

// Register handler and get cleanup function
const cleanup = ws.addHandler((message) => {
  console.log('Message:', message);
});

// Later, clean up when done
cleanup();

Security

Protect API Keys

Never expose API keys in client-side code:

// ❌ Bad: API key in browser
const ws = new WebSocket(
  'wss://nexus.fortiblox.com/geyser/ws?api-key=fbx_YOUR_KEY'
);

// ✅ Good: Use proxy server
const ws = new WebSocket('wss://your-backend.com/ws');
// Your backend adds the API key

Set up RPC Proxy →

Validate Messages

Always validate incoming messages:

function isValidMessage(message) {
  if (!message || typeof message !== 'object') return false;
  if (!message.type || typeof message.type !== 'string') return false;

  // Add more validation
  return true;
}

ws.on('message', (data) => {
  try {
    const message = JSON.parse(data);

    if (!isValidMessage(message)) {
      console.warn('Invalid message received:', message);
      return;
    }

    processMessage(message);
  } catch (error) {
    console.error('Message processing error:', error);
  }
});

Monitoring and Debugging

Add Logging

Implement comprehensive logging:

class LoggedWebSocket {
  constructor(url, apiKey) {
    this.connectionStart = Date.now();
    this.messageCount = 0;
    this.errorCount = 0;

    this.ws = new WebSocket(url, {
      headers: { 'X-API-Key': apiKey }
    });

    this.ws.on('open', () => {
      const connectTime = Date.now() - this.connectionStart;
      console.log(`Connected in ${connectTime}ms`);
    });

    this.ws.on('message', () => {
      this.messageCount++;

      // Log stats every 100 messages
      if (this.messageCount % 100 === 0) {
        console.log(`Received ${this.messageCount} messages`);
      }
    });

    this.ws.on('error', (error) => {
      this.errorCount++;
      console.error(`Error #${this.errorCount}:`, error);
    });
  }

  getStats() {
    return {
      uptime: Date.now() - this.connectionStart,
      messageCount: this.messageCount,
      errorCount: this.errorCount
    };
  }
}

Performance Metrics

Track WebSocket performance:

class MetricsCollector {
  constructor() {
    this.metrics = {
      messagesReceived: 0,
      bytesReceived: 0,
      messageLatency: [],
      lastMessageTime: null
    };
  }

  recordMessage(data) {
    this.metrics.messagesReceived++;
    this.metrics.bytesReceived += data.length;

    // Calculate latency if timestamp in message
    const message = JSON.parse(data);
    if (message.timestamp) {
      const latency = Date.now() - new Date(message.timestamp).getTime();
      this.metrics.messageLatency.push(latency);
    }

    this.metrics.lastMessageTime = Date.now();
  }

  getAverageLatency() {
    const latencies = this.metrics.messageLatency;
    if (latencies.length === 0) return 0;

    const sum = latencies.reduce((a, b) => a + b, 0);
    return sum / latencies.length;
  }

  getStats() {
    return {
      ...this.metrics,
      avgLatency: this.getAverageLatency()
    };
  }
}

Testing

Mock WebSocket for Tests

class MockWebSocket {
  constructor() {
    this.sentMessages = [];
    this.handlers = {};
  }

  send(data) {
    this.sentMessages.push(JSON.parse(data));
  }

  on(event, handler) {
    this.handlers[event] = handler;
  }

  // Simulate incoming message
  simulateMessage(data) {
    if (this.handlers.message) {
      this.handlers.message(JSON.stringify(data));
    }
  }

  // Simulate error
  simulateError(error) {
    if (this.handlers.error) {
      this.handlers.error(error);
    }
  }
}

// Usage in tests
const mockWs = new MockWebSocket();
// Test your code with mockWs

When to Use WebSocket vs REST API

Use WebSocket When:

  • ✅ You need real-time updates for all network activity
  • ✅ You're building network analytics or explorers
  • ✅ You want sub-100ms latency
  • ✅ You're processing the full transaction stream

Use REST API (Geyser) When:

  • ✅ You need specific account or program data
  • ✅ You want server-side filtering to reduce bandwidth
  • ✅ You're querying historical data
  • ✅ You only need periodic updates (not real-time)

For most account/program monitoring use cases, the Geyser REST API is more efficient until WebSocket subscription filtering is available.

Next Steps