nginx: 업로드에서 413 Request Entity Too Large
안녕하세요, BlueByte입니다. 사용자가 12MB PDF를 올리면 브라우저가 잠깐 멈췄다가 413 Request Entity Too Large가 돌아옵니다. 앱이 아니라 nginx가 보낸 응답이고, 앱은 그 파일을 구경도 못 했습니다. 고장은 아닙니다. nginx는 프록시하기 전에 본문 크기 상한을 먼저 강제하고, 그 기본값이 작을 뿐입니다. 오늘은 메시지 변형, 어느 구간이 업로드를 거절했는지 찾는 법, 한도가 살 수 있는 각 위치별 해결, 그리고 실제 파일이 도착하는지 확인하는 법까지 하나씩 짚어보겠습니다.
업로드를 거절할 때 nginx가 보내는 응답과 로그
응답 본문은 nginx 내장 오류 페이지입니다. 앱과 전혀 다르게 생긴 이유가 이것입니다:
<html>
<head><title>413 Request Entity Too Large</title></head>
<body>
<center><h1>413 Request Entity Too Large</h1></center>error.log의 짝이 되는 줄은 클라이언트가 보내려 한 크기를 알려줍니다:
2026/09/16 09:41:02 [error] 2314#2314: *118 client intended to send too large body: 12582912 bytes, client: 10.0.0.8, server: app.example.com, request: "POST /api/upload HTTP/1.1", host: "app.example.com"chunked 업로드라면 형제 메시지인 client intended to send too large chunked body: 0+12582912 bytes가 남습니다. 둘 다 [error] 레벨이고 뜻은 같습니다. 이름에 대한 참고 하나 — nginx는 여전히 예전 reason phrase를 내보내지만 RFC 9110은 413을 Content Too Large로 바꿨습니다. 같은 응답을 도구마다 다르게 표기할 수 있습니다.
기본값이 1메가바이트에서 막는 이유
client_max_body_size의 기본값은 1m입니다. nginx는 본문을 읽기 전에 요청의 Content-Length와 이 값을 비교하고, 요청이 더 크면 413으로 답하고 업로드를 버립니다 — 백엔드에는 아예 연결하지 않으며, 애플리케이션 로그가 비어 있는 이유가 이것입니다. 이 지시어는 http, server, location 컨텍스트에서 유효하고, 바로 이 상속이 발목을 잡습니다. http에 둔 값은 server나 location이 자기 값을 정하기 전까지만 모든 곳에 적용되고, 정하는 순간 그 블록에서는 안쪽 값이 이깁니다. 문서는 브라우저가 "cannot correctly display this error"라고도 적어 두었는데, 사용자가 413 대신 빈 화면을 봤다고 말하는 이유가 여기 있습니다.
어느 구간이 실제로 업로드를 거절하는지 찾기
curl로 재현하고 상태 줄만 읽습니다:
curl -s -o /dev/null -w '%{http_code}\n' \
-F 'file=@invoice.pdf' https://app.example.com/api/upload413그다음 "적용되고 있다고 믿는 파일"을 읽는 대신 nginx가 실제로 무엇을 로드했는지 물어봅니다. nginx -T는 -t와 같은 검사를 하면서 병합된 설정 전체를 덤프합니다:
sudo nginx -T 2>/dev/null | grep -n 'client_max_body_size'42: client_max_body_size 1m;출력이 아예 없으면 지시어가 어디에도 없고 기본값 1MB로 도는 중입니다. 여러 줄이 나오면 어느 것이 적용될지는 컨텍스트가 정하므로, 각각이 어느 블록에 속하는지 찾으세요. 여기 한도가 이미 넉넉한데도 413이 계속된다면 체인의 다른 곳이 거절한 것입니다 — CDN, 로드 밸런서, ingress 컨트롤러, 아니면 앱. 판정은 error.log 줄이 해 줍니다. 그 줄이 당신 서버와 바이트 수를 적고 있다면 이 nginx가 거절한 것입니다.
올바른 컨텍스트에 한도를 올리고, reload 전에 테스트하기
큰 상한이 호스트의 모든 엔드포인트에 적용되지 않도록, 업로드 경로를 덮는 가장 좁은 블록에 설정합니다:
server {
server_name app.example.com;
location /api/upload {
client_max_body_size 50m;
proxy_pass http://app_backend;
}
}문법을 먼저 검사하고 reload합니다. reload는 살아 있는 연결을 끊지 않고 설정만 다시 읽습니다:
sudo nginx -t && sudo systemctl reload nginxnginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful전역으로 올리고 싶으면 http 블록에 client_max_body_size 50m;을 두세요 — 자기 값을 가진 server나 location은 여전히 그 값을 덮어씁니다.
Kubernetes에서는 설정 파일이 아니라 annotation이 한도입니다
ingress-nginx에서는 컨트롤러 pod 안의 nginx.conf를 고쳐도 소용없습니다. 다음 동기화에서 다시 생성되기 때문입니다. 한도는 Ingress에 붙습니다:
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"적용한 뒤 실제로 렌더된 값을 확인합니다:
kubectl annotate ingress app \
nginx.ingress.kubernetes.io/proxy-body-size=50m --overwrite
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
grep -m1 client_max_body_size /etc/nginx/nginx.conf컨트롤러의 proxy-body-size 기본값은 업스트림 nginx와 같은 1m이고, 클러스터 전체에 거는 등가물은 컨트롤러 ConfigMap의 proxy-body-size 키입니다.
nginx는 무죄이고 뒤쪽 앱의 한도가 더 작을 때
nginx -T가 넉넉한 한도를 보여 주는데도 413이 남는다면 거절하는 쪽은 백엔드입니다. PHP-FPM이 대표적입니다. upload_max_filesize 기본값은 2M, post_max_size는 8M이고, PHP 문서는 post_max_size가 upload_max_filesize보다 커야 한다고 요구하므로 둘을 함께 올립니다:
upload_max_filesize = 50M
post_max_size = 52M구분하는 단서는 오류의 생김새입니다. 앱 레벨 거절은 프레임워크 오류 페이지나 JSON 본문을 그리지만, nginx의 것은 위에 보인 민짜 페이지입니다. 무엇을 바꾸기 전에 error.log와 함께 앱 로그도 확인해 보세요.
실제 사례: 백엔드에 닿지도 못한 12MB PDF
재무팀에서 인보이스 업로드가 "가끔" 실패한다고 알려 왔습니다 — 작은 파일은 되고 스캔본은 안 됩니다. 12MB 파일로 curl을 던지니 1초도 안 되어 413이 돌아옵니다. 파일이 네트워크를 건너갔다기엔 너무 빠릅니다. 엣지 호스트의 error.log에는 client intended to send too large body: 12582912 bytes가 있고, nginx -T | grep client_max_body_size는 아무것도 출력하지 않습니다. 호스트를 만든 이래 줄곧 기본값 1MB였던 것입니다. /api/upload location에 client_max_body_size 50m;을 넣고 nginx -t 후 reload하니 같은 curl이 201을 돌려줍니다. 앱 자체 한도는 이미 64MB여서 더 손댈 곳은 없었습니다.
업로드가 실제로 도착했는지 확인하고, 한도가 되돌아가지 않게 하기
상태 코드에서 멈추지 말고 바이트가 정말 갔는지 봅니다:
curl -s -o /dev/null -w '%{http_code} %{size_upload}\n' \
-F 'file=@invoice.pdf' https://app.example.com/api/upload201 12583424size_upload가 파일 크기에 가깝다면 본문이 중간에 잘리지 않고 실제로 전송된 것입니다. 이 상태를 유지하려면 한도를 호스트에서 손으로 넣지 말고 배포에 함께 나가는 설정 조각에 담고, 허용하려는 가장 큰 파일보다 조금 위로 숫자를 잡되 앱에서도 크기를 검증하고, 모든 구간 — CDN, 로드 밸런서, ingress, nginx, 앱 — 의 한도를 맞추세요. 가장 작은 값이 언제나 이깁니다. nginx 업그레이드나 설정 리팩터 뒤에는 nginx -T | grep client_max_body_size 한 줄이 2초짜리 회귀 검사가 됩니다.
413이 502나 중간에 끊기는 것과 다른 점
413은 깔끔하고 즉각적인 거절입니다. nginx가 백엔드에 연결하기 전에 결정했으므로 응답이 거의 즉시 돌아오고 업스트림 로그는 비어 있습니다. 업로드 중의 502 Bad Gateway는 nginx가 요청을 프록시했는데 백엔드가 죽거나 연결을 닫았다는 뜻이고, 해결은 client_max_body_size가 아니라 백엔드나 버퍼링 쪽에 있습니다. 504 Gateway Time-out은 본문은 받아들여졌지만 백엔드가 proxy_read_timeout보다 오래 걸려 답했다는 뜻이며, 파일 크기는 오래 걸리게 만드는 정도로만 관여합니다. 상태 코드 없이 브라우저에서 연결이 끊기며 중간에 죽는 업로드는 대개 더 바깥 — 스트림을 자르는 CDN이나 로드 밸런서 — 를 가리킵니다. 다음에 이 오류를 만나면 이 순서를 거꾸로 따라가 보세요.
관련 질문
client_max_body_size를 설정했는데도 413이 납니다.
그 값이 해당 요청을 덮지 않는 블록에 있거나, 다른 구간이 거절하는 것입니다. nginx -T | grep client_max_body_size로 실제 로드된 값과 컨텍스트를 확인하고, 그게 맞다면 CDN·로드 밸런서·ingress·앱을 차례로 보세요.
client_max_body_size 0은 무제한을 뜻하나요?
그렇습니다. nginx 문서는 size를 0으로 두면 client request body size 검사가 비활성화된다고 적고 있습니다. 다만 클라이언트가 디스크로 무제한 바이트를 흘려보내는 것을 막아 주던 방어도 함께 사라지므로, 실제 최대 파일보다 조금 큰 명시적 상한을 두는 편이 낫습니다.
사용자에게 오류 대신 빈 화면이 보이는 이유는 무엇인가요?
nginx 문서가 브라우저는 이 오류를 제대로 표시하지 못한다고 적고 있고, nginx는 응답 후 연결을 닫습니다. 프런트엔드 업로드 코드에서 413 상태를 잡아 크기 제한을 담은 메시지를 직접 보여 주세요.
한도를 바꾼 뒤 nginx를 재시작해야 하나요?
아닙니다. nginx -t && systemctl reload nginx면 충분합니다 — reload는 살아 있는 연결을 끊지 않고 설정만 다시 읽습니다. 전체 재시작도 되지만 처리 중인 요청만 끊길 뿐 얻는 것이 없습니다.
413과 Content Too Large는 같은 것인가요?
같은 상태 코드입니다. RFC 9110이 reason phrase를 Content Too Large로 바꿨지만 nginx는 여전히 Request Entity Too Large를 내보내므로, 모니터링 도구가 같은 응답을 다르게 표기할 수 있습니다.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.