⚠️ Maintenance. Notice: the site is currently in maintenance mode, so the amount of available material is temporarily limited.
Home / Tutorials / ReconnectionПереподключение

Reconnection with exponential backoffПереподключение с экспоненциальной задержкой

The problemПроблема

A WebSocket will close for reasons outside your control: Wi-Fi blips, a laptop sleeping, a mobile handoff between cells, a server redeploy. The native WebSocket object does not reconnect on its own. If you want a connection that feels permanent, you have to rebuild it — carefully.

The naive fix — reconnect immediately in onclose — is dangerous. If the server is down, thousands of clients reconnecting in a tight loop become a self-inflicted denial of service the moment it comes back. The cure is exponential backoff with jitter.

WebSocket закроется по причинам вне вашего контроля: моргнул Wi-Fi, ноутбук уснул, телефон переключился между сотами, сервер выкатил релиз. Нативный объект WebSocket не переподключается сам. Если нужно соединение, которое ощущается постоянным, его придётся пересоздавать — аккуратно.

Наивное решение — переподключаться сразу в onclose — опасно. Если сервер лежит, тысячи клиентов в плотном цикле переподключения превращаются в самостоятельно устроенный DoS в момент, когда сервер поднимется. Лекарство — экспоненциальная задержка с джиттером.

Backoff and jitterBackoff и джиттер

Each failed attempt doubles the wait — 1s, 2s, 4s, 8s — up to a ceiling. Jitter adds a random fraction so clients don't all retry on the same tick (the “thundering herd”).

backoff.js
function nextDelay(attempt, base = 1000, cap = 30000) {
  const exp = Math.min(cap, base * 2 ** attempt);
  // full jitter: random point in [0, exp]
  return Math.random() * exp;
}

Каждая неудачная попытка удваивает ожидание — 1с, 2с, 4с, 8с — до потолка. Джиттер добавляет случайную долю, чтобы клиенты не повторяли попытку в один и тот же момент («thundering herd»).

backoff.js
function nextDelay(attempt, base = 1000, cap = 30000) {
  const exp = Math.min(cap, base * 2 ** attempt);
  // полный джиттер: случайная точка в [0, exp]
  return Math.random() * exp;
}

A reconnecting wrapperКласс-обёртка с переподключением

This small class reconnects automatically, resets the counter on success, and queues messages sent while offline so nothing is lost.

reconnecting-socket.js
class ReconnectingSocket {
  constructor(url, { onMessage } = {}) {
    this.url = url;
    this.onMessage = onMessage;
    this.attempt = 0;
    this.queue = [];
    this.closedByUser = false;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      this.attempt = 0;                // reset backoff
      this.queue.splice(0).forEach(m => this.ws.send(m));
    };

    this.ws.onmessage = (e) => this.onMessage?.(e.data);

    this.ws.onclose = () => {
      if (this.closedByUser) return;
      const delay = nextDelay(this.attempt++);
      setTimeout(() => this.connect(), delay);
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
    else this.queue.push(data);   // buffer until reconnected
  }

  close() { this.closedByUser = true; this.ws.close(); }
}

Этот небольшой класс переподключается автоматически, сбрасывает счётчик при успехе и буферизует сообщения, отправленные офлайн, чтобы ничего не потерялось.

reconnecting-socket.js
class ReconnectingSocket {
  constructor(url, { onMessage } = {}) {
    this.url = url;
    this.onMessage = onMessage;
    this.attempt = 0;
    this.queue = [];
    this.closedByUser = false;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      this.attempt = 0;                // сброс backoff
      this.queue.splice(0).forEach(m => this.ws.send(m));
    };

    this.ws.onmessage = (e) => this.onMessage?.(e.data);

    this.ws.onclose = () => {
      if (this.closedByUser) return;
      const delay = nextDelay(this.attempt++);
      setTimeout(() => this.connect(), delay);
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) this.ws.send(data);
    else this.queue.push(data);   // буфер до переподключения
  }

  close() { this.closedByUser = true; this.ws.close(); }
}

Respect the close codeУчитывайте код закрытия

Not every close should trigger a retry. Code 1000 is a normal closure; 1008/4401-style application codes may mean “your token is invalid — stop trying.” Inspect event.code and bail out of the loop for permanent failures, otherwise you reconnect forever against a server that will always reject you.

Не каждое закрытие должно вызывать повтор. Код 1000 — нормальное закрытие; прикладные коды вида 1008/4401 могут означать «токен недействителен — прекратите попытки». Проверяйте event.code и выходите из цикла при постоянных ошибках, иначе будете вечно переподключаться к серверу, который всегда вас отвергает.

Bonus: react to the networkБонус: реагируйте на сеть

The browser tells you when connectivity returns. Trigger an immediate reconnect on the online event instead of waiting out the backoff timer:

online.js
addEventListener("online", () => socket.connect());
addEventListener("offline", () => console.log("link down"));

Pair this with a heartbeat to catch half-open sockets the OS hasn't noticed yet.

Браузер сообщает, когда связь восстановилась. Запускайте немедленное переподключение по событию online, не дожидаясь таймера backoff:

online.js
addEventListener("online", () => socket.connect());
addEventListener("offline", () => console.log("link down"));

Сочетайте это с heartbeat, чтобы ловить «полуоткрытые» сокеты, которые ОС ещё не заметила.