2026年11月12日

2026年11月12日

WebSocket Disconnects Behind a Proxy: Fixes for nginx, Apache and LBs

This article is a localized edition of the Japanese original. Read the Japanese original

When WebSocket connections drop through a proxy, the cause falls into one of two broad groups: "the initial connection (the upgrade) isn't making it through the proxy" or "after connecting, something along the path cuts the connection after a certain time". You can almost always tell which by checking in your browser's developer tools whether the connection was established, and if so, how many seconds after that it dropped. This article explains how to read the symptoms and walks through checking and fixing the causes in order of likelihood.

Note that WordPress core (PHP) has no mechanism for handling persistent WebSocket connections. When you implement chat or notifications over WebSocket, the usual setup is to run a separate process such as Node.js and route traffic to it from nginx or Apache. This article assumes that setup.

Read the symptoms first

Open the Network tab in your browser's developer tools, filter by type "WS", then reload the page. Select the connection row to see its status and the messages exchanged.

  • The status never becomes 101 Switching Protocols and ends with 400, 404, 426, 502 or similar: the upgrade stage is failing
  • It reaches 101, but drops at around exactly 60 seconds or another round number of seconds: an idle timeout on a device along the path
  • It drops at the moment of a deploy or server reload, regardless of how long it has been open: disconnection caused by a process restart

On the JavaScript side, logging the code of the close event helps you narrow things down. 1006 means "closed without receiving a close frame" and is commonly seen when a proxy or network device cuts the TCP connection. When the server application closes the connection normally, you would usually see 1000, 1001 or similar.

You can also check from the server side with curl whether the upgrade gets through. Connecting over HTTP/2 changes how the upgrade behaves, so add --http1.1.

curl --http1.1 -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  https://example.com/ws/

If HTTP/1.1 101 Switching Protocols comes back, the upgrade is getting through. If not, send the same request directly to the application, bypassing the proxy (for example http://127.0.0.1:3000/ws/), to determine whether the problem lies with the proxy or the application.

Cause 1: The proxy isn’t forwarding the upgrade headers

This is the most common cause. Starting a WebSocket requires the Upgrade and Connection headers, but these are treated as "hop-by-hop headers" that are valid only for a single leg, and proxies normally don't forward them. In nginx, you need to explicitly use HTTP/1.1 and pass both headers.

# Put this in the http context (outside the server block)
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    # ...your existing listen and ssl settings...

    location /ws/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

Putting map inside server causes a syntax error. Also, if you connect to a path that doesn't match location /ws/ (for example /ws without the trailing slash), this configuration won't apply. Compare the URL the client connects to with how the location is written.

In Apache, you can either use mod_proxy_wstunnel or, in newer versions (reportedly 2.4.47 and later), use the upgrade=websocket option of mod_proxy_http.

# mod_proxy and mod_proxy_http must be enabled
ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/" upgrade=websocket
ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"

Older Apache versions don't support the upgrade option, so configure it with the ws:// scheme combined with mod_proxy_wstunnel. Check the version with apachectl -v before deciding which one you can use. Before changing the configuration, save a copy of the configuration file, and check the syntax with nginx -t or apachectl configtest before reloading.

Cause 2: An idle timeout is cutting the connection

After connecting, if no messages flow for a while, devices along the path decide the connection is "unused" and cut it. nginx's proxy_read_timeout defaults to 60 seconds, so if the connection "drops at exactly one minute", suspect this first.

However, simply extending the timeout often doesn't solve the problem. Besides nginx, the path can include a cloud load balancer, a CDN, a corporate proxy, a home router and other devices, each with its own timeout, and the connection drops at the shortest one. Check the typical ones:

  • AWS Application Load Balancer: has an idle timeout attribute, 60 seconds by default, which you can extend as needed
  • Google Cloud external Application Load Balancer: for WebSocket, the backend service timeout is treated as "the maximum time a connection can stay open". The connection drops at that time whether or not it is idle
  • CDNs (Cloudflare, etc.): check each provider's documentation for whether there is a setting to pass WebSocket through, and whether there is a limit on connection duration or disconnection on restarts

Defaults and limits for each service can change (as of October 2026), so check the latest values in the documentation of the service you use.

The fundamental fix is to send messages periodically from the application. Send a small message from the server or the client at an interval shorter than the shortest timeout on the path (for example every 25 to 30 seconds). The browser's WebSocket API can't send protocol-level ping frames, so when sending from the client, use an application-level "ping" message that the server either discards or answers with a "pong".

Cause 3: A process restart or reload is cutting the connection

Application deploys, Node.js process restarts, swapping load balancer targets and the like will drop open WebSocket connections. This can't be avoided, so the client must have reconnection logic. Reconnecting immediately and repeatedly concentrates load on the server, so gradually increase the interval.

function connect(url, attempt = 0) {
  const ws = new WebSocket(url);
  let pingTimer;

  ws.addEventListener('open', () => {
    attempt = 0;
    // Send an application-level ping every 25 seconds
    pingTimer = setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 25000);
  });

  ws.addEventListener('close', (event) => {
    clearInterval(pingTimer);
    console.log('closed', event.code, event.reason);
    // Back off to 1s, 2s, 4s..., with a maximum of 30s
    const delay = Math.min(30000, 1000 * 2 ** attempt);
    setTimeout(() => connect(url, attempt + 1), delay);
  });

  return ws;
}

connect('wss://example.com/ws/');

Cause 4: Sessions don’t match in a multi-server setup

With libraries such as Socket.IO that first connect via HTTP long polling and then switch to WebSocket, if you have multiple servers, the first request and the next one can reach different servers and fail. If your server logs show 400 errors such as "session not found", enable sticky sessions on the load balancer, or configure the client to connect using WebSocket only from the start.

Summary

  • First check in the developer tools' WS view whether the status reaches 101 and how many seconds it takes to drop
  • For upgrade failures, review the proxy's Upgrade and Connection headers and HTTP/1.1 settings
  • If it drops after a fixed time, find the shortest timeout among nginx, the load balancer and the CDN, and send messages at a shorter interval than that
  • Disconnections from restarts can't be avoided, so give the client reconnection logic with an increasing interval
  • In multi-server setups, check how the switch from long polling relates to sticky sessions

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲