Node.js에서 WebSocket 연결 유지 시간 초과
Configuring WebSocket keep-alive, ping/pong intervals, and timeout handling in Node.js to maintain persistent connections.
Node.js에서 WebSocket 연결 유지 시간 초과
Introduction
WebSocket connections are designed to be persistent, but network infrastructure like load balancers, proxies, and firewalls often impose idle timeouts. Without proper keep-alive mechanisms, your WebSocket connections will silently die, causing frustrating disconnections for users.
Environment
node --version
# v20.11.0
npm list ws
# my-app@1.0.0
# +-- ws@8.16.0Problem
WebSocket connections were dropping after exactly 60 seconds of inactivity:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('close', () => {
console.log('Client disconnected unexpectedly');
// Logs every 60 seconds of inactivity
});
});Client connected
Client disconnected unexpectedly ← after 60 seconds of no messages
Client connected
Client disconnected unexpectedly ← same pattern repeatsThe 60-second timeout corresponds to the default idle timeout of the Nginx reverse proxy in front of the Node.js server.
Analysis
WebSocket connections are maintained at the TCP level. When there is no data flowing, intermediary network devices consider the connection idle and may close it. The fix is to implement application-level ping/pong keep-alive.
The WebSocket protocol includes built-in ping and pong frames:
Client → Server: ping frame
Server → Client: pong frameIf no pong is received within a timeout period, the connection is considered dead.
Solution
Solution 1: Server-side ping/pong
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
const HEARTBEAT_TIMEOUT = 10000; // 10 seconds
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log('Terminating dead connection');
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
wss.on('close', () => {
clearInterval(heartbeat);
});Solution 2: Client-side heartbeat
// client.js
class WebSocketClient {
constructor(url) {
this.url = url;
this.pingInterval = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.on('open', () => {
console.log('Connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
});
this.ws.on('pong', () => {
console.log('Pong received');
});
this.ws.on('close', () => {
this.stopHeartbeat();
this.attemptReconnect();
});
this.ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
}
startHeartbeat() {
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, 30000);
}
stopHeartbeat() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
}
attemptReconnect() {
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => this.connect(), delay);
}
}
}Solution 3: Using the ws library with proper configuration
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: false,
maxPayload: 1024 * 1024, // 1MB max message size
heartbeat: {
interval: 30000,
timeout: 10000
}
});
// Enable per-connection heartbeat tracking
wss.on('connection', (ws, req) => {
ws.isAlive = true;
ws.lastActivity = Date.now();
ws.on('message', (data) => {
ws.lastActivity = Date.now();
handleMessage(ws, data);
});
ws.on('pong', () => {
ws.isAlive = true;
ws.lastActivity = Date.now();
});
});Solution 4: Handle Nginx timeout configuration
# nginx.conf
upstream websocket {
server 127.0.0.1:8080;
}
server {
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Increase timeouts for WebSocket connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}Lessons Learned
- Always implement ping/pong - Do not rely on TCP keep-alive alone
- Set ping interval shorter than proxy timeout - 30 seconds works for most setups
- Implement exponential backoff for reconnection - Prevents reconnection storms
- Configure reverse proxies to allow long-lived WebSocket connections
- Monitor connection health - Track active connections and disconnect rates
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.