WebSocket RPC connections drop primarily due to idle timeouts enforced by load balancers or proxies, network interruptions, or server-side policy (close codes 1006, 1008, 1001). To fix, implement a heartbeat (ping/pong) to keep the connection alive, and build a reconnection strategy with exponential backoff and jitter. Diagnose using browser devtools or curl with verbose output to see close codes and timing.
The Direct Answer: Why WebSocket RPC Disconnects
If your JSON-RPC WebSocket connection keeps dropping, the root cause is almost always one of three things: an idle timeout enforced by a load balancer or proxy, a network-level interruption (NAT, mobile carrier), or a server-side policy that closes connections that don't send periodic pings. The close codes you see — 1006 (abnormal closure), 1008 (policy violation), or 1001 (going away) — are your primary diagnostic clues.
The fix is twofold: implement a heartbeat (ping/pong) to keep the connection alive, and build a reconnection strategy with exponential backoff and jitter. This article explains the mechanism behind each cause, how to diagnose your specific situation, and how to implement a robust client that survives disconnects gracefully.
Understanding WebSocket Close Codes in RPC Context
WebSocket close codes are standardized in RFC 6455. When a connection closes, the client or server sends a close frame with a code. In RPC scenarios, you'll commonly encounter:
- 1006 (Abnormal Closure): The connection closed without a close frame. This typically indicates a network issue — the TCP connection was dropped, a proxy timed out, or a firewall killed the connection. It's the most common code for RPC disconnects.
- 1008 (Policy Violation): The server closed the connection because the client violated a policy, such as sending too many requests, using an unsupported protocol, or failing to respond to pings within a timeout.
- 1001 (Going Away): The server is shutting down or the client is navigating away. In RPC, this can happen during server maintenance or when the endpoint is being redeployed.
When you see 1006, it's often a silent killer: the connection just dies, and your client may not even know until it tries to send a request and fails. This is why heartbeats are critical — they force the client to detect dead connections quickly.
- 1006: abnormal closure, no close frame — network/proxy issue
- 1008: policy violation — server rejected your behavior
- 1001: going away — server maintenance or shutdown
The Mechanism: Idle Timeouts and Load Balancers
Most RPC providers, including OnFinality, place WebSocket endpoints behind load balancers and proxies. These components often enforce idle timeouts to free up resources. For example, an AWS ALB has a default idle timeout of 60 seconds for WebSocket connections, after which it closes the connection if no data is exchanged. Similarly, nginx has a proxy_read_timeout that defaults to 60 seconds.
The key insight is that idle means no data frames are sent. WebSocket ping/pong frames count as data, so sending a ping every 30 seconds (or less) prevents the timeout from triggering. Without a heartbeat, your connection will be killed after the idle period, and you'll see a 1006 close code.
Another mechanism is NAT timeouts. If your client is behind a home router or mobile carrier, the NAT mapping may expire after a period of inactivity (often 30-120 seconds). When the mapping expires, incoming frames can't reach your client, and the connection appears dead. A heartbeat keeps the NAT mapping alive.
Diagnosing Your Disconnect: Tools and Commands
To diagnose why your WebSocket RPC disconnects, you need to observe the close code and timing. Here are two practical methods:
1. Browser DevTools (for web apps): Open the Network tab, filter by WS, and click on the WebSocket connection. The 'Messages' tab shows the frames sent and received. The 'Close' event will show the code and reason. If you see 1006, note the time since the last frame — that's your idle timeout.
2. Command-line with websocat or wscat: Install wscat (Node.js) or websocat (Rust) and connect to your RPC endpoint. Send a JSON-RPC request and then wait. Observe when the connection closes. Example:
wscat -c wss://eth-mainnet.public.blastapi.io
# After connecting, send: {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}
# Then wait and see if the connection closes after ~60 seconds
If the connection closes after a fixed interval, it's an idle timeout. If it closes randomly, it could be network instability. You can also use curl with -v to see the WebSocket handshake and close frames, though curl doesn't maintain the connection.
For a more detailed analysis, use tcpdump to capture packets and look for TCP FIN or RST packets. A RST indicates a hard reset, often from a firewall or proxy.
wscat -c wss://eth-mainnet.public.blastapi.io
# Send: {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}
# Observe close code and timingImplementing Heartbeat and Keepalive
The standard solution is to implement a WebSocket heartbeat using ping/pong frames. The WebSocket protocol supports ping and pong control frames. The client sends a ping, and the server must respond with a pong. If no pong is received within a timeout, the connection is considered dead.
In JavaScript (Node.js or browser), the ws library provides built-in ping/pong handling. Here's a minimal example:
const WebSocket = require('ws');
const ws = new WebSocket('wss://your-rpc-endpoint');
let isAlive = true;
ws.on('open', () => {
console.log('Connected');
// Send a ping every 30 seconds
const interval = setInterval(() => {
if (isAlive) {
ws.ping();
} else {
clearInterval(interval);
ws.terminate();
}
}, 30000);
});
ws.on('pong', () => {
isAlive = true;
});
ws.on('close', (code, reason) => {
console.log(`Closed with code ${code}: ${reason}`);
// Reconnect logic here
});
For Python, the websockets library has a ping_interval parameter. Example:
import asyncio
import websockets
async def main():
async with websockets.connect('wss://your-rpc-endpoint', ping_interval=30, ping_timeout=10) as ws:
# Your RPC calls
pass
asyncio.run(main())
The ping_interval sends a ping every 30 seconds, and ping_timeout waits 10 seconds for a pong. If no pong, the connection is closed and you can reconnect.
Important: Some RPC providers may not respond to pings if they are not implemented correctly. Test with your provider. If pings are not supported, you can send a lightweight JSON-RPC request (e.g., eth_blockNumber) as a keepalive, but this consumes rate limits. Prefer ping/pong when possible.
Building a Resilient Reconnection Strategy
Even with heartbeats, connections will drop. A robust client must automatically reconnect with exponential backoff and jitter to avoid overwhelming the server.
Exponential backoff: After a disconnect, wait a short time (e.g., 1 second), then double the wait on each subsequent failure (1s, 2s, 4s, 8s, ...) up to a maximum (e.g., 60s). Jitter adds randomness to the wait time to prevent thundering herd problems when many clients reconnect simultaneously.
Here's a JavaScript implementation using ws and a simple backoff:
function connectWithBackoff() {
let attempt = 0;
const maxDelay = 60000;
function connect() {
const ws = new WebSocket('wss://your-rpc-endpoint');
ws.on('open', () => {
attempt = 0;
console.log('Connected');
// Setup heartbeat as above
});
ws.on('close', (code, reason) => {
console.log(`Closed: ${code} ${reason}`);
const delay = Math.min(1000 * Math.pow(2, attempt), maxDelay) + Math.random() * 1000;
attempt++;
setTimeout(connect, delay);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
ws.close();
});
}
connect();
}
In Python, you can use the backoff library or implement manually. The key is to never reconnect immediately in a tight loop; always wait at least a second.
Also, consider using a library that handles reconnection automatically, such as reconnecting-websocket for JavaScript or websocket-client with auto-reconnect for Python. However, understand the underlying logic so you can tune it.
WebSocket vs HTTP for RPC: When to Use Which
WebSocket is ideal for real-time, bidirectional communication, such as subscribing to blockchain events (e.g., eth_subscribe). HTTP is simpler and stateless, suitable for one-off requests. If your application only needs occasional queries, HTTP is more reliable and easier to scale. If you need push notifications or streaming, WebSocket is necessary.
The trade-off: WebSocket connections are stateful and require keepalive and reconnection logic. HTTP requests are stateless and can be retried easily. For blockchain RPC, many developers use WebSocket for subscriptions and HTTP for regular calls. This hybrid approach reduces the risk of disconnects affecting critical operations.
For more on choosing the right endpoint, see our RPC endpoints guide and multi-chain RPC endpoints guide.
Common Pitfalls and Provider-Specific Behaviors
Pitfall 1: Not handling the 'close' event properly. Some clients only listen for 'error' and miss the 'close' event. Always handle both.
Pitfall 2: Sending pings too frequently. Some servers may rate-limit pings. Stick to 30 seconds or more.
Pitfall 3: Ignoring close codes. If you see 1008, it's a policy violation — check your request rate and payload size. If you see 1001, the server is going away; wait longer before reconnecting.
Provider-specific behaviors: Some providers, like Infura or Alchemy, have documented idle timeouts. For example, Infura's WebSocket connections may close after 60 seconds of inactivity. OnFinality's WebSocket endpoints also have idle timeouts; we recommend a heartbeat interval of 30 seconds or less. Check your provider's documentation for specific limits.
If you're using a public RPC endpoint, be aware that they may have stricter limits. For production, consider a dedicated endpoint via our RPC pricing page or API service.
Verifying Your Fix: Expected Behavior
After implementing heartbeats and reconnection, verify that your connection stays alive for extended periods. Use a script that logs the connection state and timestamps. Expected behavior:
- The connection remains open for hours without any close events.
- If a network interruption occurs, the client detects it within the ping timeout (e.g., 10 seconds) and reconnects with backoff.
- The close code on reconnection is typically 1006 (abnormal) or 1001 (server maintenance), and your client handles it gracefully.
You can simulate a network drop by disconnecting your Wi-Fi or using kill -STOP on the process. Observe the reconnection behavior.
Next Steps and Further Reading
Now that you understand why WebSocket RPC disconnects and how to fix it, you can apply these patterns to your own applications. For more RPC troubleshooting, see our articles on fixing RPC timeout errors and reducing RPC latency.
If you're building on Ethereum or Solana, check our network pages: Ethereum and Solana. For a broader overview of RPC providers, read our best RPC provider guide.
Finally, consider monitoring your WebSocket connections with tools like monitoring RPC endpoints to catch issues early.