WebSocket keeps a single TCP connection open for a long time and exchanges frames in both directions without repeated HTTP requests — for a chat, live notifications, or an admin panel with real-time updates. Nginx as a reverse proxy does not support Upgrade connections without explicit configuration: without it, a WebSocket request simply does not get past an ordinary HTTP proxy.
Why WebSocket does not work through proxy_pass out of the box
A regular proxy_pass in nginx is set up for an HTTP request-response cycle, while WebSocket starts as an HTTP request with an Upgrade: websocket header, after which the connection must switch to two-way frame exchange. By default nginx does not forward the Upgrade header to the backend and keeps the connection under the rules of a regular HTTP proxy, which breaks the switch to the WebSocket protocol.
The base setup of the location and proxy_pass for a site on a VDS does not change and is covered in the article on configuring Nginx. For WebSocket, only a few directives are added on top of it, but without them the connection drops right after the handshake.
Setting up the map and the Upgrade/Connection headers
The scheme recommended in the nginx documentation is to declare a map that turns the value of the Upgrade header into the value of the Connection header: if Upgrade is present, Connection becomes upgrade, and if the header is missing (a regular HTTP request to the same location), Connection is closed explicitly.
The location itself gets proxy_http_version 1.1 — HTTP/1.0 does not support Upgrade — and both headers are forwarded to the backend through proxy_set_header.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
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 $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Timeouts: why the connection drops after a minute
Three directives matter here, and their nginx defaults are too short for a long-lived connection.
| Directive | Default value | Why change it for WebSocket |
|---|---|---|
| proxy_read_timeout | 60 seconds | drops the connection if there is no data from the backend for longer than that |
| proxy_send_timeout | 60 seconds | the same, but for sending data to the backend |
| proxy_connect_timeout | 60 seconds | matters less for an already long-lived connection, but is still worth checking |
If the connection still drops with a 502 or 504 code after the timeouts are increased, the cause is worth tracking down with the article on diagnosing 502 and 504 — most often the problem is in the backend itself, not the timeout.
The WebSocket server on the application side
The actual WebSocket server that accepts the connection after nginx is a separate application, not part of PHP-FPM: a regular php-fpm process handles one request and then ends, which does not fit a long-lived connection.
An example of such a server in PHP with the Ratchet library is covered in the article on WebSockets in PHP with Ratchet. In this setup nginx proxies the connection to the port that this specific process listens on, not php-fpm.
Checklist before running WebSocket behind nginx
Before putting a WebSocket connection into production behind nginx, it is worth checking the following.
- proxy_http_version is set to 1.1, otherwise Upgrade will not go through even with the right headers.
- The map for Connection covers both cases — with and without Upgrade — for a location that serves both regular HTTP requests and WebSocket.
- proxy_read_timeout and proxy_send_timeout are increased enough not to drop chat or live-update connections too early.
- The application side implements ping/pong or another liveness check, instead of relying only on nginx timeouts.
- Load balancing between several WebSocket processes, if there is any, accounts for session stickiness — a client must not jump between processes mid-connection.
Frequently asked questions
Is a separate server block needed for WebSocket?
No, a separate location in the same server block that serves the main site is usually enough — for example /ws/. A full HTTP site and a WebSocket endpoint can live in one config without conflict, as long as the Upgrade and Connection headers are set up specifically for that location.
What if there is also a load balancer in front of nginx?
The load balancer in front of nginx must itself support proxying Upgrade connections and must not drop long-lived TCP sessions by its own timeouts — otherwise tuning at the nginx level will not help, since the problem sits one level higher.
How to check that WebSocket actually works through nginx?
The simplest way is with curl using headers that simulate the handshake, or through the browser's developer console, where the connection status and frame exchange are visible. A 101 Switching Protocols response code in the nginx logs confirms that the switch to WebSocket succeeded.