Skip to content
Last updated

Maintain reliable WebSocket connections for production subscriptions. Handle reconnections, monitor health, and manage resources efficiently.


Setup

Node.js

import { createClient } from 'graphql-ws';
import WebSocket from 'ws';

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  webSocketImpl: WebSocket,
  keepAlive: 10000,
  retryAttempts: Infinity,
  shouldRetry: () => true
});

React/Apollo

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

const httpLink = new HttpLink({
  uri: 'https://graph.kadindexer.io/graphql'
});

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'wss://graph.kadindexer.io/graphql',
    retryAttempts: Infinity,
    keepAlive: 10000
  })
);

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
});

Reconnection Strategy

Exponential Backoff

Prevent server overload during connection issues:

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  retryAttempts: Infinity,
  retryWait: async (retries) => {
    const baseDelay = 1000;
    const maxDelay = 30000;
    const jitter = Math.random() * 1000;
    
    const delay = Math.min(
      baseDelay * Math.pow(2, retries) + jitter,
      maxDelay
    );
    
    await new Promise(resolve => setTimeout(resolve, delay));
  }
});

Backoff progression:

  • Attempt 1: ~1s
  • Attempt 2: ~2s
  • Attempt 3: ~4s
  • Attempt 4: ~8s
  • Attempt 5+: ~30s (capped)

Connection State Tracking

Monitor connection status for UI feedback:

let connectionState = 'disconnected';

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  on: {
    connecting: () => {
      connectionState = 'connecting';
      updateUI('Connecting...');
    },
    connected: () => {
      connectionState = 'connected';
      updateUI('Connected');
    },
    closed: (event) => {
      connectionState = 'disconnected';
      console.error('Disconnected:', event.reason);
      updateUI('Disconnected');
    }
  }
});

Automatic Resubscription

Restore active subscriptions after reconnection:

const activeSubscriptions = new Map();

function subscribe(query, variables, handlers) {
  const id = `${query}_${JSON.stringify(variables)}`;
  
  // Remove old subscription if exists
  if (activeSubscriptions.has(id)) {
    activeSubscriptions.get(id).unsubscribe();
  }
  
  const unsubscribe = client.subscribe(
    { query, variables },
    {
      next: handlers.next,
      error: (error) => {
        console.error('Subscription error:', error);
        
        // Retry after delay
        setTimeout(() => {
          subscribe(query, variables, handlers);
        }, 5000);
      },
      complete: () => {
        activeSubscriptions.delete(id);
        if (handlers.complete) handlers.complete();
      }
    }
  );
  
  activeSubscriptions.set(id, { unsubscribe, query, variables });
  
  return () => {
    unsubscribe();
    activeSubscriptions.delete(id);
  };
}

Health Monitoring

Keepalive Configuration

Maintain connection with periodic pings:

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  keepAlive: 10000, // Ping every 10 seconds
  on: {
    ping: (received) => {
      if (!received) console.log('Sent ping');
    },
    pong: (received) => {
      if (received) console.log('Received pong');
    }
  }
});

Recommended keepAlive values:

  • 10000ms (10s) - Standard
  • 30000ms (30s) - Low-frequency updates
  • 5000ms (5s) - High-reliability requirements

Latency Tracking

Monitor subscription performance:

const metrics = {
  messageCount: 0,
  totalLatency: 0,
  lastMessageTime: null
};

client.subscribe({ query }, {
  next: (data) => {
    const now = Date.now();
    
    if (metrics.lastMessageTime) {
      const latency = now - metrics.lastMessageTime;
      metrics.totalLatency += latency;
    }
    
    metrics.messageCount++;
    metrics.lastMessageTime = now;
    
    const avgLatency = metrics.totalLatency / metrics.messageCount;
    
    if (avgLatency > 5000) {
      console.warn(`High latency: ${avgLatency}ms`);
    }
    
    processData(data);
  }
});

Error Classification

Handle errors appropriately by type:

function handleSubscriptionError(error) {
  if (error.message.includes('rate limit')) {
    console.error('Rate limit exceeded');
    // Reduce subscription count or upgrade tier
  } else if (error.message.includes('timeout')) {
    console.error('Connection timeout');
    // Trigger reconnection
  } else if (error.extensions?.code === 'UNAUTHENTICATED') {
    console.error('Authentication failed');
    // Refresh credentials
  } else {
    console.error('Unknown error:', error);
    // Generic handling
  }
}

Resource Management

Subscription Lifecycle

Prevent memory leaks with proper cleanup:

class SubscriptionManager {
  constructor() {
    this.subscriptions = new Set();
  }
  
  subscribe(query, variables, handlers) {
    const unsubscribe = client.subscribe(
      { query, variables },
      handlers
    );
    
    this.subscriptions.add(unsubscribe);
    
    return () => {
      unsubscribe();
      this.subscriptions.delete(unsubscribe);
    };
  }
  
  unsubscribeAll() {
    this.subscriptions.forEach(unsub => unsub());
    this.subscriptions.clear();
  }
  
  getCount() {
    return this.subscriptions.size;
  }
}

// Usage
const manager = new SubscriptionManager();

const unsub1 = manager.subscribe(query1, vars1, handlers1);
const unsub2 = manager.subscribe(query2, vars2, handlers2);

// Cleanup
manager.unsubscribeAll();

Connection Pooling

Share single WebSocket connection across subscriptions:

// ✅ Single shared client
const sharedClient = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  keepAlive: 10000
});

// Multiple subscriptions use one connection
const unsub1 = sharedClient.subscribe(query1, handlers1);
const unsub2 = sharedClient.subscribe(query2, handlers2);
const unsub3 = sharedClient.subscribe(query3, handlers3);

// ❌ Don't create multiple clients
const client1 = createClient({ url: '...' });
const client2 = createClient({ url: '...' });
const client3 = createClient({ url: '...' });

Graceful Shutdown

Clean up before application exit:

// Node.js
process.on('SIGTERM', async () => {
  console.log('Shutting down...');
  
  // Close all subscriptions
  manager.unsubscribeAll();
  
  // Dispose client
  await client.dispose();
  
  process.exit(0);
});

// Browser
window.addEventListener('beforeunload', () => {
  manager.unsubscribeAll();
  client.dispose();
});

Rate Limit Management

Monitor Active Subscriptions

Track subscription count against tier limits:

const tierLimits = {
  free: 5,
  developer: 25,
  team: Infinity
};

const currentTier = 'developer';
let activeCount = 0;

function canSubscribe() {
  return activeCount < tierLimits[currentTier];
}

function subscribe(query, variables, handlers) {
  if (!canSubscribe()) {
    throw new Error(`Subscription limit reached: ${activeCount}/${tierLimits[currentTier]}`);
  }
  
  activeCount++;
  
  const unsubscribe = client.subscribe({ query, variables }, {
    ...handlers,
    complete: () => {
      activeCount--;
      if (handlers.complete) handlers.complete();
    }
  });
  
  return () => {
    unsubscribe();
    activeCount--;
  };
}

Prioritize Critical Subscriptions

Drop low-priority subscriptions when hitting limits:

const subscriptions = new Map();

function subscribe(query, variables, handlers, priority = 0) {
  const id = generateId();
  
  // Check limit
  if (subscriptions.size >= tierLimit) {
    // Find lowest priority subscription
    const [lowestId, lowestSub] = [...subscriptions.entries()]
      .sort((a, b) => a[1].priority - b[1].priority)[0];
    
    if (priority > lowestSub.priority) {
      console.log(`Dropping subscription ${lowestId} (priority ${lowestSub.priority})`);
      lowestSub.unsubscribe();
      subscriptions.delete(lowestId);
    } else {
      throw new Error('Cannot add lower priority subscription');
    }
  }
  
  const unsubscribe = client.subscribe({ query, variables }, handlers);
  
  subscriptions.set(id, { unsubscribe, priority });
  
  return () => {
    unsubscribe();
    subscriptions.delete(id);
  };
}

// Usage
subscribe(criticalQuery, vars, handlers, 10); // High priority
subscribe(optionalQuery, vars, handlers, 1);  // Low priority

Backpressure Handling

Process high-frequency updates without blocking:

const messageQueue = [];
let processing = false;
const BATCH_SIZE = 10;

client.subscribe({ query }, {
  next: (data) => {
    messageQueue.push(data);
    processQueue();
  }
});

async function processQueue() {
  if (processing || messageQueue.length === 0) return;
  
  processing = true;
  
  while (messageQueue.length > 0) {
    const batch = messageQueue.splice(0, BATCH_SIZE);
    await processBatch(batch);
  }
  
  processing = false;
}

async function processBatch(batch) {
  // Process multiple messages efficiently
  const results = await Promise.all(
    batch.map(data => processData(data))
  );
  
  updateUI(results);
}

Production Patterns

Connection Health Check

Verify connection before critical operations:

async function ensureConnected() {
  if (connectionState !== 'connected') {
    throw new Error('Not connected to Kadindexer');
  }
}

async function subscribeToCriticalEvent() {
  await ensureConnected();
  
  return client.subscribe({ query: criticalQuery }, handlers);
}

Metrics Export

Track connection metrics for monitoring:

const metrics = {
  connections: 0,
  disconnections: 0,
  errors: 0,
  messagesReceived: 0,
  subscriptionCount: 0,
  avgLatency: 0
};

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  on: {
    connected: () => {
      metrics.connections++;
      metrics.subscriptionCount = activeSubscriptions.size;
    },
    closed: () => {
      metrics.disconnections++;
      metrics.subscriptionCount = 0;
    }
  }
});

// Export for monitoring (Prometheus, DataDog, etc.)
setInterval(() => {
  console.log('WebSocket metrics:', JSON.stringify(metrics));
}, 60000);

Circuit Breaker

Stop attempting connections after repeated failures:

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED';
    this.nextAttempt = Date.now();
  }
  
  canAttempt() {
    if (this.state === 'OPEN') {
      if (Date.now() >= this.nextAttempt) {
        this.state = 'HALF_OPEN';
        return true;
      }
      return false;
    }
    return true;
  }
  
  recordSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  recordFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
      console.error('Circuit breaker opened - stopping connection attempts');
    }
  }
}

const breaker = new CircuitBreaker();

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  shouldRetry: () => breaker.canAttempt(),
  on: {
    connected: () => breaker.recordSuccess(),
    closed: () => breaker.recordFailure()
  }
});

Troubleshooting

Connection Fails Immediately

Check:

  • URL: wss://graph.kadindexer.io/graphql
  • WebSocket support in environment
  • Firewall/proxy allows WebSocket traffic
  • Client supports graphql-ws protocol

Debug:

const client = createClient({
  url: 'wss://graph.kadindexer.io/graphql',
  on: {
    error: (error) => {
      console.error('Connection error:', error);
    }
  }
});

Frequent Disconnections

Solutions:

  • Implement exponential backoff
  • Increase keepAlive (10-30 seconds)
  • Check network stability
  • Verify tier rate limits

Missing Messages

Check:

  • Subscription is active during event
  • Filters aren't too restrictive
  • Connection state is 'connected'
  • Not exceeding tier subscription limits

High Memory Usage

Solutions:

  • Close unused subscriptions
  • Limit data requested per subscription
  • Implement proper cleanup on unmount
  • Use SubscriptionManager for tracking

Checklist

Before production:

  • Exponential backoff configured
  • Keepalive enabled (10-30s)
  • Connection state tracked
  • Automatic resubscription implemented
  • Proper cleanup on exit
  • Subscription count monitored
  • Error handling by type
  • Metrics exported
  • Resource limits respected

Next Steps

Need help? toni@hackachain.io