Ethereum WebSocket RPC connections drop due to idle timeouts, max connections, and network issues. This article explains the underlying mechanisms and provides reconnection patterns for ethers.js and web3.js, including a runnable example and diagnostic steps.
Direct Answer: Why Your Ethereum WebSocket RPC Keeps Disconnecting
If you're using Ethereum WebSocket RPC endpoints and experiencing frequent disconnects, the root cause is typically one of three things: idle timeouts enforced by the provider or load balancer, connection limits, or network instability. The solution is to implement a robust reconnection strategy that automatically resubscribes to your desired events. This article explains the mechanics behind eth_subscribe over WSS, why connections drop, and how to build resilient clients with ethers.js and web3.js.
In short, WebSocket connections are not permanent; they are subject to timeouts and resource limits. By understanding these constraints and coding for reconnection, you can maintain a reliable stream of blockchain data.
How Ethereum WebSocket Subscriptions Work
Ethereum nodes expose a WebSocket JSON-RPC API that supports the eth_subscribe method. This method allows clients to subscribe to real-time events such as new block headers, pending transactions, and logs. The node sends a subscription ID and then pushes notifications as events occur.
The WebSocket connection is a persistent TCP connection with a WebSocket upgrade. Both the client and server can close it. The server (node or a proxy in front of it) may close the connection due to inactivity, resource limits, or maintenance. The client may also close it due to network changes or application logic.
For example, geth's WebSocket server has settings like --ws to enable it, --ws.addr to bind address, --ws.port, and --ws.origins to restrict allowed origins. The default --ws.origins is localhost, which can cause disconnects if your client's origin is not allowed. Also, geth has an idle timeout for WebSocket connections, which is not configurable via CLI but is set to 60 seconds in some versions. This means if no messages are sent for 60 seconds, the server may close the connection. This is a common cause of disconnects for subscriptions that are not chatty, like pending transactions on a quiet network.
For more details, refer to the geth JSON-RPC documentation.
Common Causes of WebSocket Disconnects
Several factors can cause your Ethereum WebSocket RPC connection to drop:
- Idle timeouts: Many providers and load balancers close idle connections after a certain period (e.g., 60 seconds). If your subscription doesn't receive frequent events, the connection may be closed.
- Max connections: Nodes and providers limit the number of concurrent WebSocket connections per IP or per client. Exceeding this limit can cause new connections to be rejected or existing ones to be dropped.
- Provider-side maintenance: Infrastructure providers may restart nodes or perform maintenance, causing all connections to drop.
- Network issues: Unstable internet connections, firewalls, or NAT timeouts can also terminate WebSocket connections.
- Client-side issues: Bugs in your code, such as not handling ping/pong frames, can cause the connection to be considered dead.
- geth WebSocket settings: As mentioned,
--ws.originsand idle timeouts can affect connection stability.
Reconnection Patterns in ethers.js
ethers.js provides a WebSocketProvider class that wraps a WebSocket connection. However, it does not automatically reconnect. You need to implement reconnection logic yourself. The recommended pattern is to listen for the close event and then attempt to reconnect with exponential backoff.
Here is a runnable example that demonstrates a simple reconnection loop:
const { ethers } = require('ethers');
const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // Replace with your endpoint
let provider;
let shouldReconnect = true;
let retryCount = 0;
const maxRetries = 10;
function createProvider() {
provider = new ethers.WebSocketProvider(WS_URL);
provider._websocket.on('close', (code, reason) => {
console.log(`WebSocket closed: code=${code}, reason=${reason}`);
if (shouldReconnect) {
const delay = Math.min(1000 * 2 ** retryCount, 30000); // Exponential backoff
retryCount++;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(createProvider, delay);
}
});
provider._websocket.on('error', (error) => {
console.error('WebSocket error:', error);
});
// Resubscribe to events after reconnection
provider.on('block', (blockNumber) => {
console.log('New block:', blockNumber);
});
}
createProvider();
// Graceful shutdown
process.on('SIGINT', () => {
shouldReconnect = false;
provider.destroy();
process.exit();
});
In this example, we create a new provider on close, and we resubscribe to the 'block' event. Note that we use provider._websocket to access the underlying WebSocket object, which is not officially documented but works in ethers.js v5. In ethers.js v6, the API may differ; check the documentation.
Another approach is to use the WebSocketProvider's on('error') event to detect connection issues and trigger reconnection. However, the close event is more reliable for detecting disconnects.
Reconnection Patterns in web3.js
web3.js also provides a WebSocket provider, but it has built-in reconnection options. When creating a Web3 instance, you can pass a WebsocketProvider with clientConfig that includes reconnect and delay options.
Example:
const Web3 = require('web3');
const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // Replace
const provider = new Web3.providers.WebsocketProvider(WS_URL, {
clientConfig: {
// Enable auto reconnection
reconnect: {
auto: true,
delay: 5000, // ms
maxAttempts: 10,
onTimeout: false
}
}
});
const web3 = new Web3(provider);
// Subscribe to new block headers
const subscription = web3.eth.subscribe('newBlockHeaders', (error, result) => {
if (error) console.error(error);
console.log('New block header:', result);
});
// Handle provider errors
provider.on('error', (error) => {
console.error('Provider error:', error);
});
provider.on('connect', () => {
console.log('Connected');
});
provider.on('end', () => {
console.log('Connection ended');
});
The reconnect option in web3.js handles automatic reconnection, but you may still need to resubscribe to events after reconnection. The connect event can be used to re-establish subscriptions.
Note that web3.js's auto-reconnect may not resubscribe automatically; you need to handle that in the connect event.
Event-Driven Resubscribe Pattern
A robust pattern is to separate the connection logic from the subscription logic. On every (re)connection, you should resubscribe to all desired events. This ensures that after a reconnect, you don't miss any data.
Here's a conceptual pattern:
let subscriptions = [];
function setupSubscriptions(provider) {
// Clear existing subscriptions
subscriptions.forEach(sub => sub.unsubscribe());
subscriptions = [];
// Subscribe to new blocks
const sub = provider.on('block', (blockNumber) => {
console.log('New block:', blockNumber);
});
subscriptions.push(sub);
// Subscribe to logs (example)
const filter = { address: '0x...' };
const logSub = provider.on(filter, (log) => {
console.log('Log:', log);
});
subscriptions.push(logSub);
}
// Call setupSubscriptions after each connection
This pattern ensures that after a reconnect, all subscriptions are re-established. It also allows you to manage subscriptions centrally.
Diagnostic: Fetch Recent Blocks and Log Close Code
To diagnose why your WebSocket connection is dropping, you can write a script that fetches recent blocks and logs the close code and reason. This helps identify whether the disconnect is due to idle timeout, server shutdown, or other reasons.
Here's a runnable diagnostic script using ethers.js:
const { ethers } = require('ethers');
const WS_URL = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'; // Replace
const provider = new ethers.WebSocketProvider(WS_URL);
provider._websocket.on('close', (code, reason) => {
console.log(`Close code: ${code}`);
console.log(`Close reason: ${reason}`);
// Common codes: 1000 (normal), 1006 (abnormal), 1011 (server error)
});
provider._websocket.on('error', (error) => {
console.error('Error:', error);
});
// Fetch recent blocks
async function fetchRecentBlocks() {
const latest = await provider.getBlockNumber();
console.log('Latest block:', latest);
for (let i = latest; i > latest - 5; i--) {
const block = await provider.getBlock(i);
console.log(`Block ${i}: timestamp=${block.timestamp}`);
}
}
fetchRecentBlocks().catch(console.error);
// Keep the process alive
setInterval(() => {}, 1000);
Run this script and observe the close code. If you see code 1006, it means the connection was abnormally closed, possibly due to a network issue or server timeout. If you see 1000, it was a normal closure, perhaps due to idle timeout.
You can also monitor the time between messages to see if the connection drops after a period of inactivity.
Common Failures and Fixes
Here are common issues and their fixes:
- Idle timeout: Send a ping frame periodically or use a subscription that generates frequent events. Some providers allow you to set a custom keep-alive interval.
- Max connections: Ensure you are not opening multiple connections from the same IP. Use a single connection and multiplex subscriptions.
- Provider maintenance: Implement reconnection with exponential backoff and jitter to avoid overwhelming the server.
- geth origins: If you are running your own geth node, set
--ws.originsto*or your specific origin to avoid connection rejections.
- Client-side bugs: Ensure you handle the
pingandpongframes correctly. Most WebSocket libraries do this automatically, but if you are using a raw WebSocket, you need to implement it.
- Network issues: Use a reliable internet connection and consider using a WebSocket proxy that handles reconnections.
Tradeoffs and Limitations
Reconnection logic adds complexity to your application. You need to handle resubscription, avoid duplicate events, and manage state. Also, automatic reconnection may not be suitable for all use cases, such as when you need to process events in order without gaps.
Another limitation is that WebSocket connections are not as reliable as HTTP for request-response patterns. If you only need occasional data, consider using HTTP JSON-RPC instead.
Also, note that some providers may have different behavior regarding WebSocket timeouts. Always check the provider's documentation for specific settings.
Next Steps and Further Reading
Now that you understand the causes and solutions for Ethereum WebSocket disconnects, you can implement a robust reconnection strategy in your applications. For more advanced patterns, consider using libraries like reconnecting-websocket or ws with built-in reconnection.
If you're looking for a reliable Ethereum RPC provider, check out OnFinality's Ethereum network page and our RPC Assistant to find the best endpoint for your needs. Our API service offers robust WebSocket support with automatic reconnection handling.
For more troubleshooting tips, see our general WebSocket RPC disconnection fixes guide. And don't forget to review the geth JSON-RPC documentation for server-side settings.
Explore more articles on OnFinality Learn to deepen your understanding of blockchain infrastructure.