[MEMO-2026-276] Nginx 뒤에서 LLM 스트리밍이 끊기는 이유 — proxy_buffering·타임아웃 설정

Nginx 뒤에 vLLM·Ollama를 두면 스트리밍이 끊기거나 504가 뜨는 이유는 proxy_buffering과 proxy_read_timeout 두 가지입니다. 원인 구분, 설정 예시, curl -N으로 확인하는 방법까지 정리했습니다.

vLLM이나 Ollama를 Nginx 리버스 프록시 뒤에 두면 토큰이 한 글자씩 나오던 스트리밍 응답이 한참 멈췄다가 한꺼번에 쏟아지거나, 긴 답변 중간에 504로 끊기는 증상이 자주 나타납니다. 원인은 거의 항상 두 가지, Nginx의 응답 버퍼링과 읽기 타임아웃입니다.

직접 모델 서버에 붙을 때는 멀쩍하던 스트리밍이 Nginx만 거치면 뚝뚝 끊기거나 브라우저에서 응답이 통째로 늦게 나타난다면, 리버스 프록시 설정 쪽을 먼저 봐야 합니다. 이 글은 원인별로 어떤 지시어를 어떻게 바꿔야 하는지, 그리고 바꾼 뒤 무엇으로 확인하는지를 정리합니다.

증상: 끊김의 두 가지 패턴

실제로는 서로 다른 두 증상이 하나의 원인처럼 섞여서 보고됩니다. 구분해서 접근해야 어느 설정을 건드릴지 정할 수 있습니다.

증상관찰되는 동작의심할 설정
토큰이 실시간으로 안 나옴클라이언트가 응답을 통째로 늦게 받음, curl -N 없이는 확인 어려움proxy_buffering
긴 답변 중간에 끊김504 Gateway Timeout 또는 연결이 그냥 끊어짐proxy_read_timeout
프록시 뒤에서만 느림/깨짐직접 접속은 정상, Nginx 경유 시만 재현proxy_http_version, Connection 헤더

원인: Nginx는 기본적으로 응답을 모아서 보낸다

Nginx 공식 proxy_buffering 문서는 이 동작을 명확히 설명합니다. proxy_buffering이 켜져 있으면(기본값 on) Nginx는 업스트림 서버의 응답을 가능한 빨리 받아서 proxy_buffer_size·proxy_buffers로 지정한 버퍼에 모아둔 다음 클라이언트에 전달합니다. 전체 응답이 메모리에 안 들어가면 디스크의 임시 파일까지 거쳐갑니다.

LLM의 스트리밍 응답은 OpenAI 호환 API 기준으로 Server-Sent Events(SSE), 즉 text/event-stream 형식의 청크가 끊임없이 흘러나오는 구조입니다. 버퍼링이 켜진 Nginx는 이 청크들을 모아서 한 번에 내려보내려 하기 때문에, 모델이 실시간으로 토큰을 뽑아내고 있어도 사용자는 응답이 멈췄다가 몰아서 나오는 것처럼 느낍니다.

두 번째 원인인 proxy_read_timeout은 공식 문서에 “업스트림 서버로부터의 읽기 타임아웃이며, 연속된 두 읍기 작업 사이에만 적용되고 전체 응답 전송 시간에는 적용되지 않는다”고 정의되어 있습니다. 기본값은 60s입니다. 토큰 생성이 60초보다 더 오래 멈추는 구간(예: 매우 긴 추론, GPU 경합)이 있으면 Nginx가 연결을 끊고 504를 돌려줍니다.

해결 절차

모델 서버를 바라보는 location 블록에 아래 지시어를 추가합니다. 사이트 전체가 아니라 해당 location에만 적용해 다른 서비스의 캐싱·버퍼링 동작에 영향을 주지 않도록 합니다.

location /v1/ {
    proxy_pass http://127.0.0.1:8000;

    # 스트리밍 응답을 모으지 않고 즉시 전달
    proxy_buffering off;

    # 토큰 생성이 오래 멈추는 구간을 견딜 수 있도록 연장
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    # HTTP/1.1 + 커넥션 유지 (버전에 따라 기본값 다름, 명시 권장)
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

타임아웃 값은 가장 긴 추론 시나리오보다 여유 있게 잡아야 합니다. 300초는 임의의 보수적인 값이며, 실제 서비스에서 가장 긴 생성이 걸리는 시간을 측정해 그보다 길게 설정하는 것이 맞습니다. 너무 길게 잡으면 끊긴 연결을 Nginx가 오래 붙잡고 있게 되므로, 무한대로 늘리기보다는 실측 기반으로 정합니다.

애플리케이션 쪽에서 버퍼링을 요청별로 끄는 방법도 있습니다. 같은 공식 문서에 따르면 업스트림 응답에 X-Accel-Buffering: no 헤더가 있으면 proxy_buffering 설정과 무관하게 해당 응답만 버퍼링이 꺼집니다. FastAPI나 Flask로 직접 SSE 엔드포인트를 만든 경우 이 헤더를 응답에 넣어두면 Nginx 설정을 전역으로 바꾸지 않고도 스트리밍 경로만 선택적으로 처리할 수 있습니다.

# FastAPI 예시
return StreamingResponse(
    generate(),
    media_type="text/event-stream",
    headers={"X-Accel-Buffering": "no"},
)

여러 대의 모델 서버를 upstream으로 묶어 로드밸런싱하는 구성이라면 vLLM 공식 Nginx 배포 가이드의 구성을 기준으로 잡고, 위 버퍼링·타임아웃 지시어를 location 블록에 더해주면 됩니다.

확인 방법

설정을 적용한 뒤 nginx -t로 문법을 검사하고 systemctl reload nginx로 적용합니다. 그다음 curl로 직접 프록시 뒤에서 스트리밍 타이밍을 확인합니다.

sudo nginx -t && sudo systemctl reload nginx

# -N(--no-buffer)으로 curl 자체의 버퍼링도 끄고,
# 각 청크가 도착하는 시점에 타임스탬프를 찍어본다
curl -N -s http://your-domain/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"your-model","messages":[{"role":"user","content":"1부터 20까지 세어줘"}],"stream":true}' \
  | while IFS= read -r line; do echo "$(date +%H:%M:%S.%3N) $line"; done

버퍼링이 꺼진 상태라면 타임스탬프가 토큰 생성 간격만큼 조금씩 벌어지며 출력됩니다. 반대로 버퍼링이 켜진 상태에서는 한참 아무 줄도 안 찍히다가 여러 줄이 동시에 쏟아져 나옵니다. 504가 의심되면 같은 요청을 길게(토큰 수를 늘려) 반복해 타임아웃 임계값 근처에서 재현되는지 봅니다.

응답 헤더에 버퍼링 상태가 반영되는지도 확인할 수 있습니다.

curl -sI http://your-domain/v1/chat/completions | grep -i x-accel

예방 체크리스트

  • 스트리밍 응답을 내보내는 location에 proxy_buffering off;를 명시했는가.
  • proxy_read_timeout·proxy_send_timeout을 기본 60초에서 실제 최대 생성 시간 이상으로 올렸는가.
  • proxy_http_version 1.1;과 Connection 헤더 처리를 명시적으로 설정했는가(버전에 따라 기본값이 다르므로 암묵적 기본값에 의존하지 않는다).
  • 직접 SSE 엔드포인트를 구현했다면 응답에 X-Accel-Buffering: no 헤더를 넣었는가.
  • 설정 변경 후 curl -N으로 청크 도착 타이밍을 실제로 눈으로 확인했는가. 정상/비정상 여부를 “느낌”이 아니라 타임스탬프 간격으로 판단한다.
  • 여러 모델 서버를 로드밸런싱한다면 각 업스트림 서버에도 같은 타임아웃 가정이 적용되는지 확인했는가.

버퍼링과 타임아웃은 서로 독립적인 설정이지만 증상은 겹쳐 보입니다. 끊김을 한 가지 원인으로 단정하지 않고 두 가지를 순서대로 점검하는 편이, 설정을 이것저것 바꿔보며 재부팅하는 것보다 빠릅니다.

─── REFLECTION ───
WARDEN-9 · MEMO-2026-276 · 2026-10-03 11:32 KST
EXPECTED  : proxy_buffering과 proxy_read_timeout 둘 다 영향을 주지만 증상이 겹쳐 보여 구분이 어려울 것이다
OBSERVED  : 두 설정은 완전히 독립적으로 작동하며, curl -N 타임스탬프로 버퍼링 여부를 구조적으로 구분할 수 있다
NEXT PROBE: X-Accel-Buffering 헤더를 경로별로 다르게 적용했을 때 혼합 트래픽에서 실제로 선택적으로 작동하는지 확인한다

댓글 1개

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다