본문으로 건너뛰기
개발 머꼬
개발 노트Nginx
hohyeon.dev20

WebSocket이 로컬에서는 되는데 nginx를 거치면 연결이 끊긴 이유

  • #Common Pitfall
  • #Engineering Note
  • #Nginx

문제 발생

실시간 알림을 WebSocket으로 붙였습니다. 로컬에서는 잘 붙었는데 운영에서는 연결이 즉시 끊기고 클라이언트가 폴백으로 재연결만 반복했습니다.

location /ws/ {
    proxy_pass http://127.0.0.1:3001;
}

백엔드 로그에는 일반 HTTP 요청으로 찍혀 있었습니다.

원인 분석

핵심 헤더가 전달되지 않습니다. nginx 문서가 이유를 그대로 적습니다 — UpgradeConnection을 포함한 hop-by-hop 헤더는 클라이언트에서 프록시된 서버로 전달되지 않으므로, 프록시된 서버가 클라이언트의 WebSocket 프로토콜 전환 의도를 알게 하려면 이 헤더들을 명시적으로 전달해야 합니다.

hop-by-hop 헤더는 "이 구간에서만 의미가 있는" 헤더라 프록시가 다음 구간으로 넘기지 않는 것이 HTTP의 규칙입니다. 그래서 백엔드는 업그레이드 요청인 줄 모르고 평범한 요청으로 처리했고, 101 응답이 없으니 터널이 만들어지지 않았습니다.

nginx는 이 전환을 지원합니다 — 1.3.13부터 프록시된 서버가 101(Switching Protocols)로 응답하고 클라이언트가 Upgrade 헤더로 프로토콜 전환을 요청한 경우, 클라이언트와 프록시된 서버 사이에 터널을 설정하는 특수 동작 모드를 구현합니다. 우리가 할 일은 그 조건을 만들어주는 것뿐입니다.

해결 방안

  1. 두 헤더를 명시적으로 넘깁니다.
location /ws/ {
    proxy_pass http://127.0.0.1:3001;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

nginx 1.29.7 이전 버전에서는 proxy_http_version 1.1;도 함께 필요합니다.

  1. 같은 location에 일반 요청이 섞인다면 map을 씁니다. 문서가 제시하는 형태로, 업그레이드 요청이 아닐 때 Connection: close가 되도록 분기합니다.
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
  1. 타임아웃을 늘립니다. WebSocket은 오래 열려 있는 연결입니다. proxy_read_timeout 기본값(60초) 그대로면 조용한 연결이 끊깁니다 — 애플리케이션의 ping 주기와 함께 정합니다.

  2. 버퍼링을 확인합니다. 실시간 스트리밍이라면 proxy_buffering off;가 필요한 경우가 있습니다. 다만 일반 HTTP 응답까지 끄지 않도록 location을 분리합니다.

  3. 연결 수를 감안합니다. WebSocket은 요청이 아니라 연결을 점유합니다 — worker_connections와 백엔드의 동시 연결 한도를 함께 봅니다.

  4. 실제로 확인합니다. 응답이 101 Switching Protocols인지 브라우저 네트워크 탭이나 curl -i -H "Upgrade: websocket" -H "Connection: Upgrade"로 봅니다 — 200이 오면 헤더가 안 넘어간 것입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.