Maintain reliable WebSocket connections for production subscriptions. Handle reconnections, monitor health, and manage resources efficiently.
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
});
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()
});
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)
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');
}
}
});
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);
};
}
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
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);
}
});
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
}
}
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();
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: '...' });
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();
});
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--;
};
}
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
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);
}
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);
}
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);
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()
}
});
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);
}
}
});
Solutions:
- Implement exponential backoff
- Increase
keepAlive
(10-30 seconds) - Check network stability
- Verify tier rate limits
Check:
- Subscription is active during event
- Filters aren't too restrictive
- Connection state is 'connected'
- Not exceeding tier subscription limits
Solutions:
- Close unused subscriptions
- Limit data requested per subscription
- Implement proper cleanup on unmount
- Use
SubscriptionManager
for tracking
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
- Monitor events: Real-Time Subscriptions →
- Query historical data: Query Optimization →
- Deploy safely: Production Readiness →
Need help? toni@hackachain.io