WebSocket Best Practices for Solana
TL;DR
- - WebSocket connections will drop — always implement automatic reconnection with resubscription
- - Use ping/pong heartbeats to detect dead connections before they cause silent data loss
- - Prefer accountSubscribe for specific accounts, logsSubscribe for program-wide monitoring
Solana WebSocket subscriptions provide real-time push notifications for account changes, transaction confirmations, slot updates, and more. They replace expensive polling patterns but require careful connection management to maintain reliability.
Connection management
Always implement reconnection
WebSocket connections drop. Network issues, server restarts, provider maintenance, and idle timeouts all cause disconnections. Your code must handle this gracefully.
class SolanaWebSocket {
private ws: WebSocket | null = null;
private subscriptions: Map<string, SubscriptionConfig> = new Map();
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.reconnectDelay = 1000; // Reset backoff
this.resubscribeAll(); // Restore subscriptions
};
this.ws.onclose = () => {
this.scheduleReconnect();
};
this.ws.onerror = () => {
this.ws?.close();
};
}
private scheduleReconnect() {
const delay = this.reconnectDelay + Math.random() * 1000;
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay,
);
setTimeout(() => this.connect(), delay);
}
private resubscribeAll() {
for (const [id, config] of this.subscriptions) {
this.subscribe(config);
}
}
}
Key points:
- Exponential backoff prevents hammering the server.
- Jitter avoids thundering herd on mass reconnection.
- Resubscription restores all active subscriptions after reconnect.
Heartbeat / ping-pong
Detect dead connections before they cause silent data loss:
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.ping();
this.pongTimeout = setTimeout(() => {
// No pong received — connection is dead
this.ws?.close();
}, 5000);
}
}, 30000);
}
If the server does not respond to a ping within 5 seconds, close the connection and trigger reconnection.
Subscription types
accountSubscribe
Monitor a specific account for state changes:
const subId = await connection.onAccountChange(
accountPubkey,
(accountInfo, context) => {
console.log("Slot:", context.slot, "Data:", accountInfo.data);
},
{ commitment: "confirmed" },
);
Best for: monitoring specific DEX pools, user accounts, oracle price feeds.
logsSubscribe
Monitor all transactions involving a program:
const subId = await connection.onLogs(
programId,
(logs, context) => {
console.log("Signature:", logs.signature);
console.log("Logs:", logs.logs);
},
"confirmed",
);
Best for: tracking all activity on a program, event processing, indexing.
slotSubscribe
Track slot progression:
const subId = await connection.onSlotChange((slotInfo) => {
console.log("Slot:", slotInfo.slot, "Parent:", slotInfo.parent);
});
Best for: heartbeat monitoring, leader tracking, timing-sensitive applications.
signatureSubscribe
Wait for a specific transaction to confirm:
const subId = await connection.onSignature(
signature,
(result, context) => {
if (result.err) console.log("Failed:", result.err);
else console.log("Confirmed at slot:", context.slot);
},
"confirmed",
);
This subscription auto-unsubscribes after the first notification.
Performance considerations
Subscription limits
Providers limit the number of concurrent WebSocket subscriptions per connection:
| Provider tier | Typical limit |
|---|---|
| Free | 5–20 subscriptions |
| Paid | 50–500 subscriptions |
| Dedicated | Hardware-limited |
If you need to monitor thousands of accounts, batch them or use Geyser gRPC instead.
Data volume
logsSubscribe on a popular program (Jupiter, Raydium) generates thousands of events per minute. Ensure your consumer can keep pace:
- Process messages asynchronously.
- Use a queue between the WebSocket handler and processing logic.
- Drop or aggregate events if processing falls behind.
Commitment levels
Choose the appropriate commitment:
| Commitment | Latency | Reliability |
|---|---|---|
processed | Fastest (~400ms) | May be reverted |
confirmed | ~400ms–2s | Confirmed by supermajority |
finalized | ~6–12s | Irreversible |
For most applications, confirmed is the right balance. Use processed only when you need minimum latency and can handle rare reverts.
Common pitfalls
- Silent disconnections. A WebSocket can appear connected but stop receiving data. Use heartbeats to detect this.
- Missing events during reconnection. Between disconnect and resubscribe, events are lost. For critical data, poll RPC to fill the gap after reconnection.
- Subscription ID mismatch. After reconnection, old subscription IDs are invalid. Resubscribe and update your ID tracking.
- Memory leaks. Unsubscribe from subscriptions you no longer need. Accumulated subscriptions waste server resources and may hit limits.
Gap-filling pattern
Handle missed events during disconnections:
async onReconnect() {
// Get the current state
const currentState = await connection.getAccountInfo(accountPubkey);
// Resubscribe for future updates
this.subscribe(accountPubkey);
// Process the current state as if it were an update
this.handleUpdate(currentState);
}
For transaction monitoring, use getSignaturesForAddress to catch transactions that occurred during the disconnect window.
FAQ
Why does my WebSocket disconnect every few minutes? Most providers close idle connections after 60–300 seconds. Send periodic pings to keep the connection alive. Some providers also rotate servers, causing brief disconnections.
Should I use one WebSocket connection or multiple? One connection per provider endpoint is usually sufficient. All subscriptions multiplex over a single connection. Multiple connections add complexity without benefit, unless you are near the per-connection subscription limit.
Can I use WebSockets for sending transactions? No. Solana WebSockets are read-only (subscriptions and notifications). Use HTTP RPC or QUIC for sending transactions.
Is Geyser gRPC better than WebSockets? For high-volume data (monitoring hundreds of accounts, streaming all transactions for a program), yes. Geyser provides lower latency, higher throughput, and better filtering. For monitoring a few specific accounts, WebSockets are simpler and sufficient.
Updated: