WebSocket 1006 Abnormal Closure
Symptoms
- `onclose` event fires with `event.code === 1006` and `event.wasClean === false`
- No close frame in browser DevTools Network tab — connection entry shows no response code
- Connection drops after exactly 30, 60, or 120 seconds (proxy idle timeout)
- Server process exits or crashes with no graceful shutdown, clients get 1006 immediately
- wscat shows `error: socket hang up` or `disconnected` without a code
- No close frame in browser DevTools Network tab — connection entry shows no response code
- Connection drops after exactly 30, 60, or 120 seconds (proxy idle timeout)
- Server process exits or crashes with no graceful shutdown, clients get 1006 immediately
- wscat shows `error: socket hang up` or `disconnected` without a code
Root Causes
- Reverse proxy (Nginx, AWS ALB, Cloudflare) closes idle WebSocket connections after its timeout period
- Server process crashed or was OOM-killed without sending a close frame to clients
- Network interruption (Wi-Fi drop, mobile handoff) abruptly terminates the TCP connection
- Missing ping/pong heartbeat — neither client nor server sends keep-alive frames, proxy drops the idle connection
- Cloudflare 100-second WebSocket idle timeout not overridden by ping/pong activity
Diagnosis
**Step 1 — Capture the close event in the browser**
```javascript
const ws = new WebSocket('wss://example.com/ws');
ws.onclose = (event) => {
console.log('Close code:', event.code); // 1006 = no close frame
console.log('Reason:', event.reason); // usually empty for 1006
console.log('Was clean:', event.wasClean); // false for 1006
console.log('Timestamp:', new Date().toISOString());
};
ws.onerror = (err) => console.error('WS Error:', err);
```
**Step 2 — Test with wscat from the command line**
```bash
# Install: npm install -g wscat
wscat -c wss://example.com/ws
# Note the exact time of disconnection
# If it drops at T+60s, Nginx proxy_read_timeout = 60s
```
**Step 3 — Check proxy timeout settings**
```bash
# Nginx
grep -r 'proxy_read_timeout\|proxy_send_timeout' /etc/nginx/
# AWS ALB: check idle timeout in the console
aws elbv2 describe-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:... \
| grep idle_timeout
```
**Step 4 — Inspect server-side WebSocket handler for crashes**
```bash
# Check server logs for unhandled exceptions
journalctl -u my-websocket-service -n 100 --no-pager | grep -E 'ERROR|Exception|crash|OOM'
# Check OOM kills
dmesg | grep -i 'oom\|killed process'
```
**Step 5 — Measure ping/pong round trips**
```bash
# wscat supports ping
wscat -c wss://example.com/ws --no-check
# Type: /ping
# Server should respond with a pong frame
# In Chrome DevTools: Network → WS connection → Frames tab → filter by ping/pong
```
```javascript
const ws = new WebSocket('wss://example.com/ws');
ws.onclose = (event) => {
console.log('Close code:', event.code); // 1006 = no close frame
console.log('Reason:', event.reason); // usually empty for 1006
console.log('Was clean:', event.wasClean); // false for 1006
console.log('Timestamp:', new Date().toISOString());
};
ws.onerror = (err) => console.error('WS Error:', err);
```
**Step 2 — Test with wscat from the command line**
```bash
# Install: npm install -g wscat
wscat -c wss://example.com/ws
# Note the exact time of disconnection
# If it drops at T+60s, Nginx proxy_read_timeout = 60s
```
**Step 3 — Check proxy timeout settings**
```bash
# Nginx
grep -r 'proxy_read_timeout\|proxy_send_timeout' /etc/nginx/
# AWS ALB: check idle timeout in the console
aws elbv2 describe-load-balancer-attributes \
--load-balancer-arn arn:aws:elasticloadbalancing:... \
| grep idle_timeout
```
**Step 4 — Inspect server-side WebSocket handler for crashes**
```bash
# Check server logs for unhandled exceptions
journalctl -u my-websocket-service -n 100 --no-pager | grep -E 'ERROR|Exception|crash|OOM'
# Check OOM kills
dmesg | grep -i 'oom\|killed process'
```
**Step 5 — Measure ping/pong round trips**
```bash
# wscat supports ping
wscat -c wss://example.com/ws --no-check
# Type: /ping
# Server should respond with a pong frame
# In Chrome DevTools: Network → WS connection → Frames tab → filter by ping/pong
```
Resolution
**Fix 1 — Increase Nginx proxy timeout for WebSocket**
```nginx
# /etc/nginx/sites-available/mysite
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 1 hour
proxy_send_timeout 3600s;
proxy_connect_timeout 75s;
}
```
**Fix 2 — Add server-side ping/pong heartbeat**
```python
# Django Channels example
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
self._ping_task = asyncio.create_task(self._heartbeat())
async def disconnect(self, close_code):
self._ping_task.cancel()
async def _heartbeat(self):
while True:
await asyncio.sleep(30) # ping every 30s
await self.send(text_data='{"type": "ping"}')
```
**Fix 3 — Add client-side reconnection with exponential backoff**
```javascript
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.delay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.delay = 1000; };
this.ws.onclose = (e) => {
if (e.code === 1006 || !e.wasClean) {
console.log(`Reconnecting in ${this.delay}ms...`);
setTimeout(() => this.connect(), this.delay);
this.delay = Math.min(this.delay * 2, this.maxDelay);
}
};
}
}
```
**Fix 4 — Add ping/pong to keep Cloudflare connection alive**
```javascript
// Cloudflare drops idle WS after 100s; ping every 50s
const ws = new WebSocket('wss://example.com/ws');
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 50_000);
ws.onclose = () => clearInterval(pingInterval);
```
```nginx
# /etc/nginx/sites-available/mysite
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 1 hour
proxy_send_timeout 3600s;
proxy_connect_timeout 75s;
}
```
**Fix 2 — Add server-side ping/pong heartbeat**
```python
# Django Channels example
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
self._ping_task = asyncio.create_task(self._heartbeat())
async def disconnect(self, close_code):
self._ping_task.cancel()
async def _heartbeat(self):
while True:
await asyncio.sleep(30) # ping every 30s
await self.send(text_data='{"type": "ping"}')
```
**Fix 3 — Add client-side reconnection with exponential backoff**
```javascript
class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.delay = 1000;
this.maxDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.delay = 1000; };
this.ws.onclose = (e) => {
if (e.code === 1006 || !e.wasClean) {
console.log(`Reconnecting in ${this.delay}ms...`);
setTimeout(() => this.connect(), this.delay);
this.delay = Math.min(this.delay * 2, this.maxDelay);
}
};
}
}
```
**Fix 4 — Add ping/pong to keep Cloudflare connection alive**
```javascript
// Cloudflare drops idle WS after 100s; ping every 50s
const ws = new WebSocket('wss://example.com/ws');
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 50_000);
ws.onclose = () => clearInterval(pingInterval);
```
Prevention
- Always implement a ping/pong heartbeat at 30–50 second intervals on both client and server
- Set proxy_read_timeout in Nginx to match or exceed your application's expected idle window
- Implement client-side automatic reconnection with exponential backoff for all 1006 closures
- Add server process monitoring (systemd RestartSec, Kubernetes liveness probe) to restart crashed servers
- Log all WebSocket close events server-side with code, reason, and connection duration for post-mortem analysis
- Set proxy_read_timeout in Nginx to match or exceed your application's expected idle window
- Implement client-side automatic reconnection with exponential backoff for all 1006 closures
- Add server process monitoring (systemd RestartSec, Kubernetes liveness probe) to restart crashed servers
- Log all WebSocket close events server-side with code, reason, and connection duration for post-mortem analysis