Nginx Rate Limiting이 필요한 이유
API 서버를 운영하다 보면 DDoS 공격, 브루트포스 로그인 시도, 크롤러 폭주 등 비정상적인 트래픽에 노출됩니다. 애플리케이션 레벨에서 처리하면 이미 서버 리소스를 소모한 뒤이므로, 리버스 프록시 단에서 요청을 제한하는 것이 훨씬 효율적입니다. Nginx의 ngx_http_limit_req_module과 ngx_http_limit_conn_module은 이를 위한 강력한 내장 모듈입니다.
limit_req: 요청 속도 제한
가장 핵심적인 디렉티브입니다. Leaky Bucket(누수 버킷) 알고리즘을 기반으로 초당/분당 요청 수를 제한합니다:
http {
# 1) 공유 메모리 존 정의: IP별 초당 10개 요청 허용
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 2) 로그인 엔드포인트: 더 엄격하게 분당 5개
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=5r/m;
server {
listen 80;
server_name api.example.com;
# 일반 API: burst 20개까지 대기열 허용
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
# 로그인: 엄격한 제한
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://backend;
}
}
}
주요 파라미터 설명:
| 파라미터 | 설명 |
|---|---|
| zone | 공유 메모리 이름:크기. 1MB당 약 16,000개 IP 상태 저장 |
| rate | 허용 속도. 10r/s=초당 10개, 5r/m=분당 5개 |
| burst | rate 초과 시 대기열 크기. 초과분은 503 반환 |
| nodelay | burst 내 요청을 지연 없이 즉시 처리 |
| delay=N | burst 중 N개까지는 즉시, 나머지는 지연 처리 |
burst와 nodelay/delay 동작 원리
Leaky Bucket의 동작을 정확히 이해해야 올바른 설정이 가능합니다:
# Case 1: burst 없음 → rate 초과 즉시 503
limit_req zone=api_limit;
# 10r/s → 100ms마다 1개만 통과, 나머지 전부 503
# Case 2: burst=20 (delay 없음) → 초과분 큐에 대기
limit_req zone=api_limit burst=20;
# 순간 30개 요청 → 10개 즉시 처리, 20개 큐 대기(100ms 간격 처리)
# 사용자 입장에서 응답 지연 발생
# Case 3: burst=20 nodelay → 초과분 즉시 처리 but 버킷 소모
limit_req zone=api_limit burst=20 nodelay;
# 순간 30개 → 30개 모두 즉시 처리, but 이후 버킷 충전까지 503
# 가장 일반적인 설정
# Case 4: burst=20 delay=8 → 하이브리드
limit_req zone=api_limit burst=20 delay=8;
# 처음 8개 즉시 처리, 9~30번째는 100ms 간격 지연, 31번째부터 503
운영 팁: 대부분의 API에는 burst=N nodelay 조합이 적합합니다. 순간적인 트래픽 스파이크를 허용하면서도 지속적인 과부하는 차단합니다.
limit_conn: 동시 연결 수 제한
limit_req가 “속도”를 제한한다면, limit_conn은 동시에 열린 연결 수를 제한합니다. 파일 다운로드나 WebSocket처럼 오래 유지되는 연결에 유용합니다:
http {
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
# IP당 동시 연결 최대 50개
location /downloads/ {
limit_conn conn_limit 50;
limit_conn_status 429; # 기본 503 대신 429 반환
limit_rate 1m; # 연결당 다운로드 속도 1MB/s
}
}
}
키 설계: IP 외 다양한 기준
$binary_remote_addr(클라이언트 IP)만으로는 부족한 경우가 많습니다. API 키, JWT 사용자, URI별로 차등 제한할 수 있습니다:
# API 키별 제한
map $http_x_api_key $api_key_limit {
default $binary_remote_addr; # API 키 없으면 IP로 폴백
"~." $http_x_api_key; # API 키 있으면 키 기준
}
limit_req_zone $api_key_limit zone=api_by_key:20m rate=100r/s;
# URI + IP 조합 (엔드포인트별 IP 제한)
limit_req_zone $binary_remote_addr$uri zone=per_endpoint:20m rate=5r/s;
# JWT sub 클레임 기준 (auth_request 모듈 사용 시)
limit_req_zone $http_x_user_id zone=per_user:10m rate=30r/s;
화이트리스트와 차등 제한
내부 서비스나 관리자 IP는 제한에서 제외하고, 유료·무료 사용자에게 차등 제한을 적용하는 패턴입니다:
# geo 블록으로 화이트리스트 설정
geo $rate_limit_key {
default $binary_remote_addr;
10.0.0.0/8 ""; # 내부 네트워크 → 빈 문자열 = 제한 없음
172.16.0.0/12 ""; # Docker 내부
192.168.0.0/16 ""; # 사설 네트워크
}
# 빈 문자열은 limit_req_zone 키로 사용 불가 → 자동 제외
limit_req_zone $rate_limit_key zone=external:10m rate=10r/s;
# 차등 제한: 플랜별 다른 rate
map $http_x_plan $plan_rate_key {
"premium" ""; # 프리미엄은 제한 없음
default $binary_remote_addr; # 나머지는 IP 기준 제한
}
limit_req_zone $plan_rate_key zone=tiered:10m rate=20r/s;
커스텀 에러 응답
기본 503 대신 429 Too Many Requests와 함께 JSON 에러 바디를 반환하면 API 클라이언트가 올바르게 처리할 수 있습니다:
http {
limit_req_status 429; # 상태 코드 변경
server {
# 429 에러 페이지를 내부 location으로 처리
error_page 429 = @rate_limited;
location @rate_limited {
default_type application/json;
add_header Retry-After 60 always;
add_header X-RateLimit-Limit 10 always;
return 429 '{"error":"too_many_requests","message":"Rate limit exceeded. Retry after 60 seconds.","retry_after":60}';
}
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
}
로깅과 모니터링
Rate limiting이 실제로 동작하는지 모니터링하려면 로그 레벨과 커스텀 로그 포맷을 설정합니다:
http {
# rate limit 로그 레벨 (기본 error → warn으로 낮춤)
limit_req_log_level warn;
limit_conn_log_level warn;
# 제한 상태를 access_log에 포함
log_format rate_log '$remote_addr - $status '
'$request_uri '
'limit_req_status=$limit_req_status '
'upstream_response_time=$upstream_response_time';
# $limit_req_status 변수:
# PASSED = 정상 통과
# DELAYED = burst 큐에서 지연 후 처리
# REJECTED = 503/429 반환
# DELAYED_DRY_RUN / REJECTED_DRY_RUN = dry_run 모드
server {
access_log /var/log/nginx/rate_limit.log rate_log;
}
}
Prometheus로 메트릭을 수집하려면 nginx-prometheus-exporter와 함께 stub_status를 활용하세요.
dry_run 모드로 안전하게 테스트
프로덕션에 바로 적용하기 불안하다면 limit_req_dry_run on으로 실제 차단 없이 로그만 기록할 수 있습니다:
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_dry_run on; # 차단하지 않고 로그만 기록
proxy_pass http://backend;
}
# error.log에 "limiting requests, dry run" 메시지 확인 후
# dry_run off로 전환
실전 설정 예시: 전체 구성
종합적인 프로덕션 설정 예시입니다. Nginx 리버스 프록시 가이드의 기본 설정에 rate limiting을 추가한 형태입니다:
http {
# === Rate Limit Zones ===
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=auth:5m rate=5r/m;
limit_req_zone $binary_remote_addr zone=upload:5m rate=2r/s;
limit_conn_zone $binary_remote_addr zone=download:10m;
# === 전역 설정 ===
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
# === 화이트리스트 ===
geo $limit_key {
default $binary_remote_addr;
10.0.0.0/8 "";
127.0.0.1 "";
}
server {
listen 443 ssl http2;
server_name api.example.com;
# 429 에러 핸들링
error_page 429 = @rate_limited;
location @rate_limited {
default_type application/json;
add_header Retry-After 30 always;
return 429 '{"error":"rate_limit_exceeded"}';
}
# 일반 API
location /api/ {
limit_req zone=general burst=50 nodelay;
proxy_pass http://backend;
}
# 인증 엔드포인트 (엄격)
location /api/auth/ {
limit_req zone=auth burst=3 nodelay;
proxy_pass http://backend;
}
# 파일 업로드
location /api/upload {
limit_req zone=upload burst=5;
client_max_body_size 50m;
proxy_pass http://backend;
}
# 파일 다운로드 (동시 연결 + 속도 제한)
location /files/ {
limit_conn download 10;
limit_rate 5m;
alias /var/www/files/;
}
}
}
주의사항과 베스트 프랙티스
- CDN/로드밸런서 뒤에서는
$binary_remote_addr대신$http_x_forwarded_for에서 실제 IP를 추출하세요.set_real_ip_from과real_ip_header디렉티브를 함께 설정해야 합니다. - 공유 메모리 크기를 너무 작게 잡으면 오래된 항목이 밀려나 제한이 풀립니다. 예상 동시 접속 IP 수의 2배로 설정하세요.
- rate는 보수적으로 시작하고 dry_run 로그를 분석한 뒤 조정하세요. 정상 사용자가 차단되면 신뢰를 잃습니다.
- 헬스체크 엔드포인트는 반드시 화이트리스트에 포함하여 모니터링 도구가 차단되지 않도록 하세요.
- limit_req와 limit_conn을 함께 사용하면 속도와 동시 연결 모두 제어할 수 있어 더 강력합니다.
마치며
Nginx rate limiting은 애플리케이션 코드 변경 없이 인프라 레벨에서 트래픽을 보호하는 가장 효율적인 방법입니다. Leaky Bucket 알고리즘의 burst/nodelay 동작을 정확히 이해하고, dry_run으로 충분히 테스트한 뒤 적용하세요. geo 블록으로 화이트리스트를 관리하고, 429 응답에 Retry-After 헤더를 포함하면 클라이언트 친화적인 rate limiting을 구현할 수 있습니다.