Script Valley
WebSockets & Real-Time Applications
Browser WebSocket APILesson 2.4

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.

Up next

How to close a WebSocket connection gracefully

Sign in to track progress

WebSocket heartbeat and keep-alive implementation โ€” Browser WebSocket API โ€” WebSockets & Real-Time Applications โ€” Script Valley โ€” Script Valley