Симптом: чат работает, потом через минуту-две сообщения перестают доходить, а клиент об этом не знает. Перезагрузка страницы помогает. В логах — тишина.

Это почти всегда не WebSocket, а обратный прокси, который закрывает «простаивающее» соединение.

Где именно рвётся WebSocket-соединение за nginx

Минимальный правильный блок

location /ws {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_buffering off;
}

Четыре вещи здесь важны все:

  • proxy_http_version 1.1 — без него Upgrade не пройдёт, и вы получите бесконечный 101-цикл или тихий отказ.
  • proxy_set_header Upgrade/Connection — собственно переключение протокола.
  • proxy_read_timeout — дефолт 60 секунд. Если сервер не отправляет ничего минуту, nginx прибьёт соединение. Для чата это ровно та минута, через которую «всё сломалось».
  • proxy_buffering off — иначе nginx копит ответы сервера и выдаёт их пачками.

Почему keepalive не спасает

Клиенты и серверы часто шлют ping каждые 30 секунд именно чтобы обойти таймаут. Это работает, но создаёт другую проблему: если ping не проходит, вы узнаёте об этом только по отсутствию pong — то есть должны сами считать время. В приложении обязана быть проверка: если последний pong старше двух интервалов, соединение пересоздаётся.

Как проверить, что дело в прокси

timeout 120 curl -sN -o /dev/null -w '%{http_code} %{time_total}\n' \
  -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  http://127.0.0.1:8080/ws

Если curl висит ровно 60 секунд и выходит — таймаут прокси. Если рвётся сразу — дело в бэкенде.

Ещё один слой, о котором забывают: балансировщики и облачные LB поверх nginx держат свои таймауты, и самый маленький из них и определяет реальное время жизни соединения. Ставить 3600s в nginx бессмысленно, если перед ним стоит LB с лимитом 30 секунд.