WebSocket в Nginx: почему живое соединение падает
WebSocket проходит через Nginx только при корректной передаче Upgrade-заголовков. Разбираем типичный сбой на проде: HTTP-эндпоинт отвечает, а соединение получает 400, 426 или обрывается через минуту.

На живом проекте API продолжает отвечать с кодом 200, но WebSocket-клиент получает 400 или 426, а уже открытое соединение закрывается примерно через минуту простоя. Причина часто не в самом WebSocket-сервере, а в обратном прокси: Nginx не передал Upgrade, использовал HTTP/1.0 до upstream или применил короткий таймаут. Ниже — разбор конфигурации для Nginx 1.24+ и схема проверки без выдуманных замеров: с логами, кодами ответа и наблюдением за соединениями.
Что именно ломается между браузером и сервером
Обычный HTTP-запрос заканчивается ответом. WebSocket начинается как HTTP-запрос, но затем просит сервер перейти на другой протокол внутри того же TCP-соединения. Клиент отправляет заголовки Upgrade: websocket и Connection: Upgrade. Успешный сервер отвечает 101 Switching Protocols.
Nginx по умолчанию не обязан передавать hop-by-hop-заголовки дальше. Hop-by-hop-заголовки1 относятся к конкретному соединению между двумя соседними узлами и обычно не проходят через прокси автоматически. Поэтому браузер может отправить Upgrade Nginx, но upstream без явной настройки его не увидит.
Для WebSocket недостаточно направить трафик на правильный порт. Нужно отдельно настроить переход соединения на HTTP/1.1, передать два Upgrade-заголовка и увеличить таймаут простоя.
Сноска. Upstream — сервер за прокси, к которому Nginx передаёт запрос. Это может быть приложение на Node.js, Go, Python или отдельный сервис в Kubernetes.
Конфигурация, которая должна быть на границе
Пример рассчитан на Nginx 1.24 и более новые версии с обычным HTTP/1.1-проксированием к WebSocket-сервису. Пусть приложение слушает имя chat_backend, а WebSocket доступен по пути /socket/. Блок map размещается внутри секции http, а location — внутри нужного server.
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream chat_backend {
server 127.0.0.1:8080;
}
server {
listen 443 ssl;
server_name example.com;
location /socket/ {
proxy_pass http://chat_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60m;
proxy_send_timeout 60m;
}
}
}
Здесь proxy_http_version 1.1 нужен для общения Nginx с upstream. Два последних заголовка передают сам запрос на переключение протокола. proxy_read_timeout задаёт время, в течение которого Nginx ждёт данные от upstream; это не максимальная длительность WebSocket.
После изменения сначала проверьте конфигурацию командой nginx -t, затем перечитайте её штатным способом вашей системы. Если Nginx запущен в контейнере, команду нужно выполнять внутри контейнера или через используемый в проекте механизм reload.
Разбор типичного сбоя на проекте
Симптом 1: HTTP работает, WebSocket получает 400 или 426
Это означает, что маршрут до приложения, скорее всего, существует, но сервер не получил корректный запрос на Upgrade. В конфигурации часто есть только proxy_pass:
location /socket/ {
proxy_pass http://chat_backend;
}
Для обычного API такой блок может быть достаточен. Для WebSocket — нет. Добавьте HTTP/1.1 и заголовки из рабочего примера. Затем проверьте не только ответ браузера, но и access-лог Nginx. Важная комбинация — статус клиента, статус upstream и время запроса:
log_format websocket '$remote_addr $request $status '
'upstream=$upstream_status '
'request_time=$request_time '
'upstream_time=$upstream_response_time';
Если Nginx возвращает ошибку до обращения к приложению, поле upstream может быть пустым. Если upstream отвечает 400 или 426, запрос дошёл до приложения, но заголовки, путь, Origin или авторизация не соответствуют его правилам. Это уже не исправляется одним параметром Nginx.
Симптом 2: соединение открывается, но закрывается через 60 секунд
Стандартный proxy_read_timeout Nginx рассчитан на HTTP-сценарий и может закрыть молчащее соединение. WebSocket-сессия при этом не обязательно сломана: она просто не передала данные за установленный интервал.
Есть два рабочих решения. Первое — увеличить таймаут в Nginx. Второе — добавить heartbeat на уровне приложения: клиент и сервер периодически обмениваются ping/pong или служебным сообщением. Увеличение таймаута без heartbeat оставляет мёртвые TCP-соединения незамеченными, если сеть оборвалась между пакетами.
Точный интервал выбирайте по протоколу приложения и ограничениям балансировщиков. Нельзя считать, что значение в Nginx отменяет таймаут внешнего CDN, ingress-контроллера, облачного балансировщика или мобильной сети.
Симптом 3: локально работает, в проде — 502
Код 502 обычно означает, что Nginx не получил корректный ответ от upstream. Проверьте четыре слоя по порядку:
- Доступность процесса. Слушает ли приложение нужный адрес и порт, а не только
127.0.0.1внутри другого контейнера. - Имя сервиса. Разрешается ли
chat_backendиз окружения Nginx, особенно после перезапуска контейнеров. - Путь. Совпадает ли путь, который получает приложение, с маршрутом WebSocket.
- Лимиты и перезапуски. Не убивает ли процесс supervisor, systemd, Kubernetes или лимит памяти при открытии новых соединений.
Если upstream недоступен сразу, увеличивать таймаут бессмысленно. Сначала нужен успешный TCP-коннект и ответ 101.
Где конфигурация отличается от обычного reverse proxy
| Проверка | Обычный HTTP | WebSocket |
|---|---|---|
| Версия HTTP до upstream | Часто достаточно настройки по умолчанию | Явно задайте HTTP/1.1 |
| Заголовки переключения | Не нужны | Передайте Upgrade и Connection |
| Жизненный цикл | Запрос обычно быстро завершается | Одно соединение может жить часами |
| Контроль простоя | Таймаут запроса | Таймаут чтения плюс heartbeat приложения |
| Главный успешный ответ | 200, 201 или другой код API | 101 Switching Protocols |
Отдельная ловушка — proxy_pass с URI. Запись proxy_pass http://chat_backend; передаёт исходный URI. Если добавить завершающий путь, Nginx начнёт заменять совпавшую часть URI по правилам proxy_pass. Поэтому изменение строки вроде proxy_pass http://chat_backend/ws/; может превратить ожидаемый /socket/room в другой путь. Проверяйте фактический маршрут в логах приложения.
Что проверить перед выкладкой
- В браузере в Network → WS появляется ответ
101, а не только успешный HTTP-запрос к странице. - В access-логе Nginx зафиксирован запрос к нужному URI и успешный статус upstream.
- Приложение принимает соединение с тем же Host, Origin и способом авторизации, которые использует продакшен.
- Проверены idle-таймауты всех промежуточных узлов: Nginx, ingress, CDN и балансировщика.
- При остановке приложения клиент получает закрытие и умеет переподключаться с backoff, а не создаёт бесконечный поток запросов.
- Ограничены число соединений и потребление памяти на worker приложения. Один WebSocket может занимать больше ресурсов, чем короткий HTTP-запрос.
Где подход ломается
Конфигурация выше не решает проблемы авторизации. Если токен передаётся в cookie, проверьте атрибуты cookie, домен и Secure. Если используется заголовок Authorization, убедитесь, что приложение действительно читает его на этапе WebSocket-handshake.
Не стоит без проверки отключать валидацию Origin или разрешать любой Origin. Для браузерного WebSocket это может открыть чужим сайтам доступ к пользовательской сессии, если сервер доверяет cookie.
Кластер из нескольких экземпляров приложения тоже меняет картину. После Upgrade отдельные сообщения одного соединения идут в один backend, но новые соединения распределяются заново. Если приложение хранит состояние комнаты только в памяти процесса, понадобится общий брокер или маршрутизация по сессии. Это уже архитектура приложения, а не настройка Nginx.
HTTP/2 и WebSocket нельзя автоматически считать одним и тем же режимом. Приведённая конфигурация описывает классический WebSocket через HTTP/1.1 до upstream. Если перед Nginx, между прокси или на стороне клиента используется иной транспорт, проверьте поддержку именно этой схемы в конкретной версии Nginx и вашего балансировщика.
Что делать дальше
Сначала добейтесь стабильного 101 на одном соединении и зафиксируйте его в логах. Затем добавьте heartbeat и метрики: число активных соединений, закрытия по причине таймаута, 4xx/5xx на handshake, время жизни соединения и перезапуски процесса. После этого проведите нагрузочный тест с тем числом одновременных клиентов, которое ожидается в проде, измеряя память и файловые дескрипторы Nginx и приложения. Без этих замеров увеличение таймаута остаётся догадкой, а не настройкой системы.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: NGINX WebSocket proxying, NGINX ngx_http_proxy_module
Частые вопросы
Почему WebSocket получает 426, хотя обычный сайт открывается?
Какой таймаут поставить для WebSocket в Nginx?
Что делать, если в логах Nginx 502?
Сработает ли эта конфигурация на другой версии Nginx?
Почему соединение открывается, но сообщения не доходят до нужной комнаты?
Что делать, если после настройки соединение всё равно закрывается?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.