A client can appear connected but be completely unreachable — they closed their laptop, lost network silently, or the OS killed the app. These are called zombie connections. They consume server memory indefinitely. Heartbeats detect and kill them.
ws.terminate() to force-close the zombie// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const PING_INTERVAL = 30_000; // Ping every 30 seconds
wss.on('connection', (ws) => {
ws.isAlive = true; // Mark alive on connect
// Reset alive flag every time a pong arrives
ws.on('pong', () => {
ws.isAlive = true;
});
ws.on('message', (data) => {
// handle messages...
});
});
// Heartbeat interval — runs every 30 seconds
const heartbeat = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log('💀 Zombie connection detected — terminating');
return ws.terminate(); // Force close — no graceful handshake
}
// Assume dead until proven alive
ws.isAlive = false;
ws.ping(); // Send protocol-level ping frame
});
}, PING_INTERVAL);
// Clean up when server closes
wss.on('close', () => {
clearInterval(heartbeat);
});
The browser's WebSocket API handles pong replies automatically. You don't write any pong logic on the client side.
If you want heartbeats visible in your message handler (useful for debugging or measuring latency):
// client.js — app-level ping
let pingInterval;
ws.onopen = () => {
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
timestamp: Date.now(),
}));
}
}, 25_000); // Every 25s (before server's 30s timeout)
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'pong') {
const latency = Date.now() - data.echo;
console.log(`♥️ Latency: ${latency}ms`);
return; // Don't process heartbeats as real messages
}
handleMessage(data);
};
ws.onclose = () => {
clearInterval(pingInterval);
};
// server.js — respond to app-level pings
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'ping') {
ws.send(JSON.stringify({
type: 'pong',
echo: msg.timestamp,
latency: Date.now() - msg.timestamp,
}));
return;
}
// Handle other message types...
});
Protocol Ping (ws.ping()) |
App-Level Ping (JSON) | |
|---|---|---|
| Who handles pong | Browser auto-responds | You write pong logic |
| Visible in JS | No (browser hides it) | Yes (onmessage fires) |
| Can measure latency | No | Yes |
| Overhead | Minimal (2-byte frame) | Slightly more |
| Use for | Zombie detection | Latency monitoring |