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 keyValidate 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 mockWsWhen 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.