⚠️ Maintenance. Notice: the site is currently in maintenance mode, so the amount of available material is temporarily limited.
Home / Tutorials / HeartbeatsHeartbeat

Heartbeats: ping, pong & keep-aliveHeartbeat: ping, pong и keep-alive

The silent death problemПроблема «тихой смерти»

TCP can fail without telling either side. If a phone loses signal or a NAT table entry expires, packets simply stop arriving — no close event fires. This is a half-open connection: your code still thinks readyState === 1, but messages vanish into the void.

On top of that, reverse proxies and load balancers aggressively close connections that look idle. A WebSocket carrying no traffic for 30–60 seconds is a prime candidate for an 504 or a silent drop.

TCP может отказать, не сообщив ни одной из сторон. Если телефон теряет сигнал или истекает запись в таблице NAT, пакеты просто перестают приходить — событие close не срабатывает. Это полуоткрытое соединение: код всё ещё считает, что readyState === 1, но сообщения уходят в пустоту.

Вдобавок обратные прокси и балансировщики агрессивно закрывают соединения, которые выглядят простаивающими. WebSocket без трафика 30–60 секунд — первый кандидат на 504 или тихий разрыв.

Protocol-level ping/pongPing/pong на уровне протокола

RFC 6455 defines control frames 0x9 (ping) and 0xA (pong). A browser cannot send a ping from JavaScript, but it answers server pings automatically. So the canonical pattern is: server pings, client auto-pongs, and the server drops anyone who fails to pong in time.

server-heartbeat.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });

function heartbeat() { this.isAlive = true; }

wss.on("connection", (ws) => {
  ws.isAlive = true;
  ws.on("pong", heartbeat);   // client answered our ping
});

// Sweep every 30s: anyone who didn't pong is dead.
const timer = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on("close", () => clearInterval(timer));

RFC 6455 определяет управляющие кадры 0x9 (ping) и 0xA (pong). Браузер не может послать ping из JavaScript, но автоматически отвечает на ping сервера. Поэтому канонический шаблон: сервер пингует, клиент авто-понгует, а сервер отключает тех, кто не ответил вовремя.

server-heartbeat.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });

function heartbeat() { this.isAlive = true; }

wss.on("connection", (ws) => {
  ws.isAlive = true;
  ws.on("pong", heartbeat);   // клиент ответил на наш ping
});

// Проход каждые 30с: кто не ответил pong — мёртв.
const timer = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on("close", () => clearInterval(timer));

Application-level heartbeatsHeartbeat на уровне приложения

Because the browser can't send protocol pings, clients often need their own JSON-level heartbeat to detect a dead server. Send a tiny message on a timer and expect a reply within a window:

client-heartbeat.js
let pongTimer;
function startHeartbeat(ws) {
  setInterval(() => {
    ws.send('{"type":"ping"}');
    pongTimer = setTimeout(() => ws.close(), 5000); // no pong → dead
  }, 25000);

  ws.addEventListener("message", (e) => {
    if (e.data === '{"type":"pong"}') clearTimeout(pongTimer);
  });
}
Pick an interval shorter than the most aggressive proxy timeout in your path — 25–30 seconds is a safe default. Too frequent wastes battery on mobile; too rare lets connections lapse.

Поскольку браузер не может слать протокольные ping-и, клиентам часто нужен собственный heartbeat на уровне JSON, чтобы обнаружить мёртвый сервер. Отправляйте крошечное сообщение по таймеру и ждите ответа в заданном окне:

client-heartbeat.js
let pongTimer;
function startHeartbeat(ws) {
  setInterval(() => {
    ws.send('{"type":"ping"}');
    pongTimer = setTimeout(() => ws.close(), 5000); // нет pong → мёртв
  }, 25000);

  ws.addEventListener("message", (e) => {
    if (e.data === '{"type":"pong"}') clearTimeout(pongTimer);
  });
}
Выбирайте интервал короче самого агрессивного таймаута прокси на пути — 25–30 секунд безопасны по умолчанию. Слишком часто — расход батареи на мобильных; слишком редко — соединения отваливаются.

Tie it togetherСоберите всё вместе

A heartbeat tells you a socket is dead; reconnection logic brings it back. When the client's pong timer fires, call close() — your reconnecting wrapper's onclose handler then kicks off the backoff loop. The two patterns are designed to work as a pair.

Heartbeat сообщает, что сокет мёртв; логика переподключения возвращает его к жизни. Когда срабатывает pong-таймер клиента, вызовите close() — обработчик onclose вашей обёртки запустит цикл backoff. Эти два паттерна задуманы работать в паре.