2026年5月24日

2026年5月24日

WebSocket通信がプロキシで切断される原因と対処(nginx・Apache・ロードバランサー)

WebSocket がプロキシ経由で切れる原因は、大きく「最初の接続(アップグレード)がプロキシを通れていない」と「つながった後、一定時間で経路のどこかに切られている」の2つに分かれます。どちらなのかは、ブラウザの開発者ツールで接続が確立したか、確立後何秒で切れたかを見ればほぼ判別できます。この記事では、症状の読み方と、原因を可能性の高い順に確認・修正する手順を説明します。

なお、WordPress 本体(PHP)は WebSocket の常時接続を扱う仕組みを持っていません。チャットや通知を WebSocket で実装する場合は、Node.js などの別プロセスを立てて、nginx や Apache から振り分ける構成が一般的です。この記事はその構成を前提にしています。

まず症状を読む

ブラウザの開発者ツールで Network タブを開き、種類を「WS」で絞り込んでからページを再読み込みします。接続の行を選ぶと、ステータスとやり取りしたメッセージが見られます。

  • ステータスが 101 Switching Protocols にならず、400・404・426・502 などで終わる:アップグレードの段階で失敗している
  • 101 にはなるが、ちょうど60秒前後やきりのよい秒数で切れる:途中の機器のアイドルタイムアウト
  • 一定の時間に関係なく、デプロイやサーバーの再読み込みのタイミングで切れる:プロセスの再起動による切断

JavaScript 側では close イベントの code を記録しておくと切り分けに役立ちます。1006 は「クローズフレームを受け取らずに切れた」という意味で、プロキシやネットワーク機器が TCP 接続を切ったときによく見られます。サーバーのアプリが正常に閉じた場合は、1000 や 1001 などになるのが普通です。

サーバー側からも、アップグレードが通るかを curl で確認できます。HTTP/2 で接続するとアップグレードの挙動が変わるため、--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/

HTTP/1.1 101 Switching Protocols が返れば、アップグレードは通っています。返ってこない場合は、プロキシを経由せずにアプリへ直接(例: http://127.0.0.1:3000/ws/)同じリクエストを送り、プロキシとアプリのどちらに原因があるかを分けます。

原因1:プロキシがアップグレードのヘッダーを転送していない

最も多い原因です。WebSocket の開始には Upgrade と Connection というヘッダーが必要ですが、これらは「その区間だけで有効なヘッダー」として扱われ、プロキシは通常それを次へ転送しません。nginx では明示的に HTTP/1.1 を使い、2つのヘッダーを渡す設定が必要です。

# http コンテキスト(server ブロックの外)に置く
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    # ...既存の listen や ssl の設定...

    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;
    }
}

map を server の中に書くと文法エラーになります。また、location /ws/ に一致しないパス(例: 末尾のスラッシュがない /ws)でつないでいると、この設定が適用されません。クライアントの接続先 URL と location の書き方を照らし合わせてください。

Apache では、mod_proxy_wstunnel を使う方法と、新しめのバージョン(2.4.47 以降とされています)で mod_proxy_http の upgrade=websocket 指定を使う方法があります。

# mod_proxy と mod_proxy_http が有効であること
ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/" upgrade=websocket
ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"

古いバージョンの Apache では upgrade 指定が使えないため、ws:// スキームと mod_proxy_wstunnel の組み合わせで設定します。どちらが使えるかは apachectl -v でバージョンを確認してから決めてください。設定を変える前には設定ファイルを保存し、nginx -t や apachectl configtest で文法を確認してから再読み込みします。

原因2:アイドルタイムアウトで切られている

接続後、メッセージが流れない時間が続くと、経路上の機器が「使われていない接続」と判断して切ります。nginx の proxy_read_timeout は既定で60秒なので、「ちょうど1分で切れる」場合はまずここを疑います。

ただし、タイムアウトを延ばすだけでは解決しないことが多いです。経路にはnginx 以外にも、クラウドのロードバランサー、CDN、社内のプロキシ、家庭用ルーターなど、それぞれ別のタイムアウトを持つ機器があり、最も短いものに合わせて切れます。代表的なものを確認します。

  • AWS の Application Load Balancer:アイドルタイムアウトの属性があり、既定は60秒。必要に応じて延ばせる
  • Google Cloud の外部アプリケーションロードバランサー:バックエンドサービスのタイムアウトが、WebSocket では「接続を開いておける最長時間」として扱われる。アイドルかどうかに関係なく、その時間で切れる
  • CDN(Cloudflare など):WebSocket を通す設定があるか、接続時間に上限や再起動時の切断があるかを各社のドキュメントで確認する

各サービスの既定値や上限は変更されることがあるため(2026年10月現在)、契約中のサービスのドキュメントで最新の値を確認してください。

根本的な対策は、アプリ側で定期的にメッセージを流すことです。経路上で最も短いタイムアウトより短い間隔(例: 25〜30秒ごと)で、サーバーかクライアントから小さなメッセージを送ります。ブラウザの WebSocket API からはプロトコルレベルの ping フレームを送れないので、クライアントから送る場合はアプリ独自の「ping」メッセージにし、サーバー側で読み捨てるか「pong」を返します。

原因3:プロセスの再起動や再読み込みで切られている

アプリのデプロイ、Node.js プロセスの再起動、ロードバランサーのターゲット入れ替えなどがあると、接続中の WebSocket は切れます。これは避けられないので、クライアントに再接続の処理を持たせておくことが前提になります。すぐに再接続を繰り返すとサーバーに負荷が集中するため、間隔を少しずつ延ばします。

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

  ws.addEventListener('open', () => {
    attempt = 0;
    // 25秒ごとにアプリ独自の ping を送る
    pingTimer = setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 25000);
  });

  ws.addEventListener('close', (event) => {
    clearInterval(pingTimer);
    console.log('closed', event.code, event.reason);
    // 1秒・2秒・4秒…と延ばし、上限は30秒
    const delay = Math.min(30000, 1000 * 2 ** attempt);
    setTimeout(() => connect(url, attempt + 1), delay);
  });

  return ws;
}

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

原因4:複数台構成でセッションが一致していない

Socket.IO のように、最初に HTTP のロングポーリングで接続してから WebSocket へ切り替えるライブラリでは、サーバーが複数台あると、最初のリクエストと次のリクエストが別のサーバーに届いて失敗することがあります。サーバーのログに「セッションが見つからない」といった 400 エラーが出る場合は、ロードバランサーでスティッキーセッションを有効にするか、最初から WebSocket だけで接続する設定にします。

まとめ

  • 開発者ツールの WS 表示で、101 になるかと、何秒で切れるかを最初に確認する
  • アップグレード失敗は、プロキシの Upgrade・Connection ヘッダーと HTTP/1.1 の設定を見直す
  • 一定時間で切れるなら、nginx・ロードバランサー・CDN のうち最も短いタイムアウトを探し、それより短い間隔でメッセージを流す
  • 再起動による切断は避けられないので、クライアントに間隔を延ばしながらの再接続を持たせる
  • 複数台構成では、ロングポーリングからの切り替えとスティッキーセッションの関係を確認する

お気軽にご相談ください

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