WebSocket heartbeat and keep-alive implementation
server-initiated ping, client-side pong, application-level heartbeat, setInterval pattern, heartbeat timeout detection, stale connection detection, cleanup on close
Why Connections Go Silent Without Keep-Alives
NAT routers and load balancers silently drop idle TCP connections after 30โ90 seconds. The application sees no close event โ the connection appears open but is dead. Heartbeats detect this:
const HEARTBEAT_INTERVAL = 25000; // 25 seconds const HEARTBEAT_TIMEOUT = 5000; // server must pong within 5s let heartbeatTimer = null; let pongTimer = null; function startHeartbeat(ws) { heartbeatTimer = setInterval(() => { if (ws.readyState !== WebSocket.OPEN) return; ws.send(JSON.stringify({ type: 'ping' })); pongTimer = setTimeout(() => { console.warn('Heartbeat timeout โ reconnecting'); ws.close(1001, 'heartbeat timeout'); }, HEARTBEAT_TIMEOUT); }, HEARTBEAT_INTERVAL); } ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'pong') { clearTimeout(pongTimer); return; } // ... other handlers }; ws.onclose = () => { clearInterval(heartbeatTimer); clearTimeout(pongTimer); };
The server must handle ping messages and reply with pong. Keep the interval under 30 seconds to reliably outlast most NAT timeouts. Always clear both timers in onclose to prevent memory leaks when a component re-mounts.
