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”).
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»).
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.
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(); } }
Этот небольшой класс переподключается автоматически, сбрасывает счётчик при успехе и буферизует сообщения, отправленные офлайн, чтобы ничего не потерялось.
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:
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:
addEventListener("online", () => socket.connect()); addEventListener("offline", () => console.log("link down"));
Сочетайте это с heartbeat, чтобы ловить «полуоткрытые» сокеты, которые ОС ещё не заметила.