왜 rate limiting이 필요한가

로그인 브루트포스, 크롤러 폭주, 결제 API 남용 같은 트래픽은 애플리케이션 레벨에서 막으면 이미 늦다. DB 커넥션과 워커 스레드가 소진된 뒤이기 때문이다. nginx의 limit_req는 요청이 업스트림에 닿기 전 엣지에서 걸러내므로, 백엔드 자원을 지키는 첫 방어선이 된다. 문제는 잘못 튜닝하면 정상 사용자까지 503으로 튕겨낸다는 점이다.

존(zone)의 동작 원리: leaky bucket

nginx rate limiting은 leaky bucket 알고리즘으로 동작한다. rate=10r/s는 "초당 10개"가 아니라 "100ms에 1개"로 해석된다. 즉 요청 간격을 균등하게 강제한다. 사용자가 100ms 안에 2개를 보내면 두 번째는 초과로 간주된다. 이 미세한 시간 단위가 버스트 제어를 이해하는 핵심이다.

기본 존 설정

http {
    # binary_remote_addr: IP당 16바이트, 10m이면 약 16만 IP 추적
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
        location /login {
            limit_req zone=login burst=3;
            limit_req_status 429;
        }
    }
}

존 크기 10m은 상태 저장 공간이지 rate와 무관하다. 상태가 넘치면 nginx는 오래된 항목을 제거하고, 그래도 부족하면 503을 반환하므로 실측 IP 수 기준으로 넉넉히 잡는다.

burst와 nodelay의 차이

가장 많이 틀리는 부분이다. burst는 초과 요청을 즉시 버리지 않고 대기 큐에 넣는다. nodelay 유무에 따라 처리 방식이 완전히 달라진다.

설정동작적합한 곳
burst만큐에 넣고 rate 간격대로 순차 처리(지연 발생)배치·비대화형 엔드포인트
burst + nodelay버스트 한도 내는 즉시 처리, 슬롯은 rate로 회복사용자 대면 API
burst 없음rate 초과 즉시 429엄격한 로그인·OTP

nodelay는 정상 사용자의 순간적 클릭 연타는 통과시키되, 지속적 폭주는 차단하는 균형점이다. 대부분의 REST API는 nodelay가 정답이다. 반대로 burst만 쓰면 큐 대기로 응답 지연이 쌓여 오히려 체감 품질이 나빠진다.

키 선택: IP만으로는 부족하다

$binary_remote_addr는 NAT·모바일 네트워크 뒤 다수 사용자를 한 IP로 묶어버린다. 사무실 전체가 한 사람 때문에 차단될 수 있다. 인증된 요청은 사용자 식별자를, 프록시 뒤라면 실제 IP를 키로 쓴다.

map $http_authorization $limit_key {
    default          $binary_remote_addr;   # 익명은 IP 기준
    "~.+"            $http_authorization;    # 토큰 있으면 사용자 기준
}
limit_req_zone $limit_key zone=user_api:20m rate=30r/s;

# CDN/LB 뒤라면 신뢰 프록시에서만 X-Forwarded-For 신뢰
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

real_ip 설정 없이 X-Forwarded-For를 직접 키로 쓰면 헤더 위조로 우회되니 반드시 신뢰 대역을 지정한다.

튜닝: 로그로 실제 임계값 찾기

rate 값은 감이 아니라 데이터로 정한다. 처음엔 limit_req_dry_run on;으로 차단 없이 로그만 남겨 어느 IP가 얼마나 걸리는지 관찰한다.

log_format ratelimit '$remote_addr $status $request_time '
                     '$limit_req_status "$request_uri"';

# 초과 로그만 집계해 실제 임계 검증
grep 'limiting requests' /var/log/nginx/error.log \
  | awk '{print $NF}' | sort | uniq -c | sort -rn | head

정상 사용자의 p99 요청률을 측정한 뒤 그 1.5~2배를 rate로, 순간 피크를 burst로 잡는 것이 안전한 출발점이다.

주의점

첫째, 여러 location이 같은 존을 공유하면 합산 카운트가 적용된다. 엔드포인트별 특성이 다르면 존을 분리한다. 둘째, limit_req는 워커 프로세스가 공유 메모리로 카운트하므로 단일 서버 기준이다. nginx를 수평 확장하면 노드 수만큼 실질 한도가 늘어나니, 전역 제한이 필요하면 Redis 기반 분산 제어를 병행한다. 셋째, 기본 초과 응답은 503인데 이는 서버 장애로 오인되어 모니터링을 오염시킨다. limit_req_status 429로 명시해 "속도 제한"임을 클라이언트와 대시보드 모두에 분명히 하자.