Introduction
WebSocket close code 1006 indicates an abnormal closure -- the connection was lost without a proper close frame being exchanged. Unlike clean close codes (1000, 1001), code 1006 means the underlying TCP connection was severed, typically due to network interruption, server crash, proxy timeout, or the process being killed without sending a close frame. This is the most common WebSocket error in production because real-world networks are unreliable -- mobile network switches, WiFi drops, load balancer idle timeouts, and NAT table expiration all cause silent connection drops that manifest as 1006 closures.
Symptoms
Client-side:
ws.onclose = (event) => {
console.log(`WebSocket closed: code=${event.code}, reason=${event.reason}, wasClean=${event.wasClean}`);
// Output: WebSocket closed: code=1006, reason=, wasClean=false
};Server-side (ws library):
wss.on('connection', (ws) => {
ws.on('close', (code, reason) => {
console.log(`Client disconnected: code=${code}, reason=${reason.toString()}`);
// Output: Client disconnected: code=1006, reason=
});
});Common patterns: - Connections drop after exactly 60 seconds (load balancer idle timeout) - Connections drop after periods of inactivity (NAT timeout) - Multiple clients disconnect simultaneously (network partition)
Common Causes
- Load balancer idle timeout: ALB, Nginx, or HAProxy closes idle connections after a timeout
- No heartbeat/ping-pong: Without keep-alive, idle connections are silently dropped
- Network interruption: Client loses connectivity (WiFi switch, airplane mode)
- Server restart without draining: Server killed without closing WebSocket connections
- Proxy buffering: Reverse proxy closes connection when buffer is full
- Firewall state table expiration: NAT/firewall drops connection tracking entries
Step-by-Step Fix
Step 1: Implement heartbeat ping/pong
```javascript const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => { ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (data) => { // Handle messages }); });
// Check heartbeat every 30 seconds const interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.isAlive === false) { // Client did not respond to previous ping - terminate return ws.terminate(); }
ws.isAlive = false; ws.ping(); // Send ping, expect pong response }); }, 30000);
wss.on('close', () => { clearInterval(interval); }); ```
Step 2: Client-side reconnection with exponential backoff
```javascript function createWebSocket(url) { let reconnectDelay = 1000; // Start with 1 second const maxDelay = 30000; // Max 30 seconds
function connect() { const ws = new WebSocket(url);
ws.onopen = () => { console.log('WebSocket connected'); reconnectDelay = 1000; // Reset on successful connection };
ws.onclose = (event) => {
console.log(WebSocket closed: ${event.code}, reconnecting in ${reconnectDelay}ms);
setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); connect(); }, reconnectDelay); };
ws.onerror = (error) => { console.error('WebSocket error:', error.message); ws.close(); // Triggers onclose with reconnection };
return ws; }
return connect(); }
const ws = createWebSocket('wss://api.example.com/ws'); ```
Step 3: Configure load balancer timeout to match heartbeat
```nginx # Nginx configuration location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
# Timeout must be longer than heartbeat interval proxy_read_timeout 86400s; # 24 hours proxy_send_timeout 86400s; } ```
Prevention
- Always implement server-side ping/pong heartbeat with 30-second intervals
- Set load balancer/proxy timeouts to at least 2x the heartbeat interval
- Implement client-side reconnection with exponential backoff and jitter
- Use
ws.terminate()(notws.close()) for unresponsive clients - Monitor the rate of 1006 closures -- spikes indicate network or infrastructure issues
- Add connection tracking to detect and clean up zombie connections
- Use WebSocket ping frames (not application-level messages) for heartbeat -- they are handled at the protocol level