
운영 중인 프로젝트에서는 AI에게 요청을 보내는 API나 소켓 연결을 통한 채팅 서비스를 제공하고 있다. 기능을 구현하여 서비스를 완성시키는 것이 목적이었기에 안정성보다 동작 여부에 초점을 두고 개발을 진행하였고 완성된 서비스에 대해 사용자들을 대상으로 데모를 진행하였다.
데모 단계에서 구현한 기능에 대한 동작은 오류없이 잘 동작하는 듯 보였지만, AI 요청에 대한 한도를 초과하여 요청이 거절되거나 채팅 도배로 인해 정상적인 유저들 간 소통에 장애가 발생하는 것은 직접 관찰하였고 이에 대한 피드백을 받아가며 해당 서비스들에 대한 요청 제한이 필요할 것이라 판단하였다.
때마침 백엔드 스터디에서 읽던 서적(가상 면접 사례로 배우는 대규모 시스템 설계 기초)에서도 처리율 제한장치에 대한 내용이 나와, 이를 적절히 활용하여 마주한 문제를 해결하고자 하였다.
처리율 제한 장치(Rate Limiter) 는 말 그대로 특정 시간 내에 전송되는 클라이언트의 요청 횟수를 제한하는 역할을 한다. API 요청 횟수가 Rate Limiter의 설정된 임계값을 초과하면 그 이후에 들어오는 요청에 대해서는 모두 무시된다.
그렇다면, 요청이 들어오면 그대로 처리하면 되는데 왜 들어오는 요청의 수를 조절해야 할까?
DoS(Denial of Service) 공격 방지
DoS란 악의적인 행위자가 정상적인 트래픽을 사용할 수없을 때까지 요청을 무수히 보내 자원을 고갈시키는 공격을 의미한다. API의 Rate Limiter가 존재하지 않는다면, 서버는 공격자가 보내는 모든 요청을 응답하기 위해 서버의 자원을 고갈시킬 것이며 이로 인해 정상적인 사용자는 서비스 이용에 지연 혹은 장애가 발생할 것이다.
만약 Rate Limiter가 있다면, 악의적인 유저로부터 무수히 들어오는 요청에 대해 일정시간 거절할 수 있기 때문에 서버의 자원을 정상적인 유저의 요청에 가용할 수 있다.
비용절감
요청 비용이 큰 API의 경우, 갑작스러운 트래픽 폭증으로 인해 청구 비용이 기하급수적으로 증가할 수 있는 위험이 존재한다. 혹은, Auto Scaling 서버와 같이 트래픽에 따라 인스턴스 수가 조절되는 서비스의 경우에도 갑작스러운 요청에 의해 예상치 못한 비용이 청구될 수 있다.
서버 과부하 방지
트래픽이 갑자기 몰릴 경우 서버의 자원(CPU/메모리/DB 커넥션 등)이 한계에 도달하여 과부하로 인해 전체 서비스 장애로 이어질 수 있다.
Rate Limiter는 요청을 일정 속도로 유지하며 서버가 안정적으로 요청을 처리할 수 있는 환경을 제공한다.
요청을 처리하는 알고리즘에는 여러 방식이 있는데, 학습하던 책에서 나온 내용을 기반으로 정리해보려 한다. 추가적인 학습을 진행하며 내용이 좋았던 첨부한다.
(이해한 내용을 기반으로 다이어그램을 작성하였는데, 오류가 있다면 지적 부탁드립니다!)
하나의 버킷에 일정 시간마다 토큰이 채워지며 하나의 요청을 처리할 때, 하나의 토큰이 소모된다.

클라이언트의 요청은 서비스에 도달하기 전, 처리율 제한 장치를 먼저 거치게 된다. 처리율 제한 장치가 갖고 있는 버킷은 지정된 용량의 토큰을 가지고 있으며 이 토큰은 일정한 주기마다 버킷에 추가되고 버킷은 지정된 용량을 초과할 수 없다. 하나의 요청을 처리할 때마다 토큰이 제거되고 버킷 내 토큰이 없으면 요청을 거절한다.
토큰 버킷 알고리즘은 요청에 대한 허용량만 제한하는 반면, 리키 버킷(Leaky Bucket)은 고정된 출력 속도를 가지게끔 하는 알고리즘이다. (구멍뚫린 양동이는 일정된 속도로 물이 새는 것처럼) 일정한 처리 속도를 유지하기 위해 큐(Queue)를 사용하여 FIFO(First-In First-Out) 방식으로 요청을 처리한다.

토큰 버킷 방식과 유사한 점은 버킷이라는 공간에 요청이든 토큰이든 보관하여 처리한다는 것이지만 가장 큰 차이점은 Burst한 요청의 허용 여부이다.
만약 버킷이 5 RPS(Request Per Second)를 허용한다고 할 때, 5개의 요청이 동시에 들어온다면(burst한 상황), 토큰 버킷은 5개의 요청을 한 번에 서비스로 보내 처리하지만 리키 버킷은 일정된 시간에 따라 요청을 차례대로 서비스에 보낸다.
이에 대해 리키 버킷 알고리즘은 서버가 안정적으로 요청을 받고 일관된 속도로 출력할 수 있게끔해주는 장점이 있다.
요청을 안정적으로 처리하기 위해 일정 시간의 제약을 걸어둔만큼, 단기간에 들어오는 Burst한 요청에 대해 큐가 초과되어 버려지는 요청들이 생겨날 수 있다는 점이다.
예를 들어, 처리 속도를 10 RPS로 제한한 상황에서 갑자기 초당 100개의 요청이 들어오더라도 처리 속도는 초당 10개로 고정되어 있다. 큐에 여유가 있다면 초과된 요청의 일부는 큐에 쌓이지만, 앞서 큐에 적재된 요청이 처리되기 전까지 대기해야 한다. 따라서 요청이 허용되더라도 큐에서 요청을 꺼내 처리하는 속도가 정해져 있기 때문에 지연이 발생할 수 있다. 또한 한 번 큐가 쌓이면 다음 초가 되더라도 즉시 해소되지 않고, 이후 요청에도 지연이 이어질 수 있다.
고정 윈도우 카운터는 일정 시간 주기로 나뉜 각 윈도우에서 요청을 받으면 카운터가 올라가고, 설정된 임계치를 초과하면 요청이 거절되는 알고리즘이다.

위 이미지에서 각 윈도우는 1초 단위로 생성되며 각 윈도우는 초당 4개의 카운터까지 요청을 허용한다. 만약 하나의 윈도우에, 즉 1초에 4개를 초과하는 요청이 들어오면 다음 윈도우가 생성되기 전까지 요청을 거절한다.
보기에는 간단하고 명쾌하게 이해할 수 있지만 이 알고리즘의 가장 큰 문제점은 각 윈도우가 맞닿는 경계면에서 Burst한 요청이 발생할 수 있다는 점이다.
예를 들어 위 이미지의 윈도우는 1초마다 카운터가 갱신되는데 1.30초와 2초 사이에 4개의 요청과 2초와 2.30초 사이에 4개의 요청이 발생한다면 1초 안에 총 8개의 요청이 발생하여, 기존 초당 4개의 요청을 허용한다는 제약을 위반하게 된다.
이동 윈도우 로그는 고정 윈도우 알고리즘의 문제점이었던 윈도우 경계 지점의 Burst 요청 문제를 해결해준다.
요청이 들어오는 타임스탬프 로그를 기록하며 요청을 처리하거나 거절하며, 새로운 타임스탬프가 들어오면 기존 만료된 타임스테프는 제거하고 새로 로그를 기록한다.

만약 1분 안에 3개의 요청만 허용한다고 가정해보자.
50초, 52초, 54초에 요청이 들어오면 타임스탬프를 로그에 기록하고 요청을 허용하고 1분 50초가 될 때까지 새로운 요청을 거절한다. 만약 1분 51초에 새로운 요청이 들어온다면 기존 50초에 기록된 타임스탬프 로그를 제거하고 1분 51초 로그를 추가한다.
만약 고정 윈도우 알고리즘을 사용했다면 50초와 1분의 경계에 6개의 Burst 요청이 발생할 가능성이 존재한다. 50초와 1분 사이 3개의 요청이 발생하고 1분과 1분 n초 사이에 3개의 요청이 발생한다면 초당 6개의 요청이 발생했기 때문이다.
하지만 윈도우 로깅 알고리즘에서는 타임스탬프 로그에 따라 윈도우가 움직이기 때문에 경계면이 존재하지 않으며 윈도우 길이인 1분 내 3개의 요청만 허용할 것을 엄격하게 지키게 된다.
하지만 이러한 방식에서도 타임스탬프 로그를 저장하고 관리해야 하기 때문에 로그 저장 비용이 발생한다.
이동 윈도우 카운터 알고리즘은 이동 윈도우 로깅 알고리즘과 고정 윈도우 카운터 알고리즘을 결합한 방식으로, 이동 윈도우 로깅 알고리즘의 메모리 저장 비용 문제를 타임스탬프 로깅이 아닌 카운터로 계산함으로써 해결한 방식이다.

만약 1분간 8개의 요청을 허용한다고 가정해보자.
현재 요청 시점이 1분 18초라 가정한다면 1분으로부터 30%가 지나간 상황이다. 이 때 나머지 70%에 대해 이전 1분 구간에서 발생한 요청 수를 계산하여 요청을 제한한다.
// 위 예제에서 1분부터 현재까지 3개의 요청이 발생하였고 이전 구간에서는 7개의 요청이 발생
// 현재 30%가 진행되었음으로 이전 구간의 70%만큼 값을 계산한다.
3 + (7 * 70%)
= 3 + 5(4.9 반올림)
= 8
현재 1분 18초 기준으로 이전 0초부터 1분까지 요청의 70% 요청 수를 더해 총 8개의 요청을 받은 상태다. 1분부터 1분 18초까지 3개의 요청은 허용하지만 이를 초과한 요청에 대해서는 거절한다.
이동 윈도우 카운터 알고리즘은 추정치를 계산하여 요청 제한을 처리하기 때문에 오차가 발생할 수 있다. 하지만 Cloudflare에서 실험한 결과, 40억개의 요청 중 0.003%의 오차밖에 발생하지 않았다고 한다.
Rate Limit을 할 수 있는 여러 알고리즘을 살펴보았으니, 이제 운영 중인 서비스에 어떻게 적용할 수 있을까 고민해보았다. 개요에서 언급하였듯, 현재 운영 중인 서비스에는 여러 기능들에 대한 Rate Limit이 필요하다.
1️⃣ 채팅 시스템 도배 방지
하나의 배틀 내에는 A팀, B팀, 중립으로 나뉜 채팅방이 운영된다.
채팅을 발생시키는 이벤트에 대해 Rate Limit을 설정하지 않는다면, 특정 유저로 인해 해당 채팅방에 도배가 발생될 위험이 있게 되고 정상적인 유저 간 소통이 원활히 이루어지지 않는다는 문제가 발생할 수 있다.
2️⃣ AI API 호출 제한
배틀을 생성할 때, 배틀 생성 정보를 통해 배틀 참여자들이 참고할 수 있는 정보를 AI를 통해 생성하여 제공하는 서비스가 존재한다. 현재, Gemini API(gemini-3.0-flash)을 사용하고 있으며 무료 정책을 맞추기 위해, 그리고 추후 과금 방지를 위해 Google 정책에 맞춰 요청 횟수를 제한해야 한다.
| RPM | TPM | RPD | |
|---|---|---|---|
gemini-3.0-flash | 5 | 250k | 20 |
3️⃣ 기본적인 서버 과부하 방지
전체적으로 모든 API에 대해 악의적인 공격을 방지하기 위한 용도로 요청 제한을 걸 수 있다. DoS 공격과 같이 서버를 향해 대량의 트래픽을 발생시키는 것을 방지하여 서버의 안정성을 향상시킬 수 있다.
그렇다면 Rate Limiter는 어느 곳에 위치해야 하는가?
크게 세 가지로 구분할 수 있다.
1️⃣ Client
요청을 보내기 전 클라이언트에서 계산하여 제한하는 방법이다.
2️⃣ Gateway
3️⃣ Server
이에 대해 우선적으로 모든 API 요청의 수를 조절할 수 있는 Gateway에서 Rate Limit을 설정하기로 결정하였다.
분산 서비스 환경에서는 API Gateway에 Rate Limit을 설정하여 요청 횟수를 관리할 수 있다. 하지만 현재 운영 중인 프로젝트는 Nginx를 통해 정적 파일 서빙 및 요청을 프록시를 해주고 있기 때문에 간단히 Nginx Rate Limit을 통해 요청의 수를 조절하기로 결정하였다.
Rate Limit에서 가장 중요한 것은 어느 정도의 요청을 제한할 것인가를 설정하는 것이다. 1초에 몇 개의 요청을 할 것인지, 초 단윈지, 분 단윈지 등 Limiting의 대상이 되는 임계값을 설정해야 한다.
임계값을 정확하게 설정하기 위해서는 모니터링을 통해 실제 사용자들의 요청 수를 집계하고 요청이 가장 많이 일어나는 시간대에 얼마나 빠르게 요청이 들어오는 지를 직접 확인하며 설정할 수 있어야 한다.
하지만 개인 프로젝트에서 실사용자가 직접 서비스를 이용하고 있지 않으니, 몇 가지 가정을 통해 임계값을 산출하였다.
'사용자의 요청을 어느 정도로 제한할 것인가?'에 답변하기 위해서는 서버가 수용할 수 있는 요청의 한계점을 파악할 수 있어야 한다. 서버가 어느 정도의 요청량을 수용할 수 있는지를 파악해야 안정적인 트래픽을 유지할 수 있는 Rate Limit 임계값을 설정할 수 있기 때문이다.
현재 NCP Standard(vCPU 2EA, Memory 8GB) 서버를 사용하고 있고, K6 테스트를 통해 RPS 단계를 높여가며 p95, p99 속도가 확연히 느려지는 구간을 찾으려 테스트를 진행하였다.
어느 정도의 응답 속도를 서버의 한계점으로 볼 것인가?
기본적인 요청에 대한 응답 속도가 500ms를 초과한다면 서버의 응답 속도가 늦은 상태라고 가정하기로 하였다.
RPS capacity test summary
-------------------------
Slow response threshold: 500ms
Step duration: 120s
Cooldown between steps: 20s
RPS | actual RPS | failed | slow | p95 | p99 | dropped | stable
-------|------------|-----------|-----------|--------------|--------------|----------|--------
25 | 25.01 | 0.00% | 0.00% | 25.42ms | 31.48ms | 0 | yes
50 | 50.01 | 0.00% | 0.00% | 25.82ms | 33.20ms | 0 | yes
75 | 75.01 | 0.00% | 0.00% | 27.71ms | 34.70ms | 0 | yes
100 | 100.01 | 0.00% | 0.00% | 37.70ms | 55.45ms | 0 | yes
125 | 125.01 | 0.00% | 0.00% | 57.85ms | 111.55ms | 0 | yes
150 | 149.63 | 0.00% | 14.54% | 1041.34ms | 1313.88ms | 45 | no
RPS_STEPS = 25,50,75,100,125,150 로 RPS 25부터 150까지 단계를 나누었고 각 단계 별로 2분 부하 + 20초 대기를 통해 다음 단계로 넘어가는 식으로 설계하였다.
통계치를 보았을 때 150 RPS을 넘어가는 순간
서버가 150 RPS를 넘어가면 서버가 정상적으로 응답할 수 없는 상태라 가정하였고 125 RPS 환경은 서버의 정상적인 응답 속도 기준을 만족하기 때문에 125 RPS를 서버가 감당할 수 있는 요청의 수용량이라 설정하였다.
(150 RPS를 한계치로 설정하면, Rate Limit의 최댓값으로 들어올 경우 서버가 불안정해질 수 있기 때문에, 안정적인 응답 속도를 유지하며 수용할 수 있는 요청의 최대치로 설정하였다.)
서버의 한계치를 파악했으니 이를 기준으로 Rate Limit 임계값을 설정할 차레다. 앞에서 언급했듯, 현재 Rate Limit은 Nginx에서 설정하는 것이기 때문에 IP당 요청 허용량을 설정해야 한다. 하지만, 우리 서비스에는 실사용자가 사실상 없기 때문에(🥲) 가정을 통해 임계값을 잡았다.
위에서 설정하였던 서버 전체 RPS는 125로 설정하였고 최대 동시 접속자 수는 40명으로 설정했으니, 하나의 IP당 3개의 요청까지 허용한다면 서버가 안정적으로 응답할 수 있을 것이라 판단하여 IP별 RPS는 3으로 가정하였다.
서버의 한계치도 알고 Rate Limit의 임계값도 산출하였으니 이제 Nginx에 Rate Limit을 적용하는 단계다. 적용하기 앞서, Nginx는 Rate Limit을 어떻게 제공하는지 설명하려 한다.
Nginx는 Leaky Bucket 알고리즘을 통해 Rate Limit 기능을 제공한다.

간단히 다시 설명하자면, Leaky Bucket 알고리즘은 말그대로 양동이 위로 물이 부어지면 양동이의 아래쪽으로 물이 새는 것처럼 동작한다. 물이 새는 속도의 최대값은 정해져있으며 물이 새는 속도보다 물이 부어지는 속도가 더 빠르다면, 양동이는 더이상 물을 담을 수 없게 된다.
양동이 -> FIFO 버퍼
물 -> Client 요청
새는 속도 -> 요청 처리 속도
Nginx에서는 Rate Limit을 설정하기 위해 limit_req_zone와 limit_req 문법을 제공한다.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
location /login/ {
# limit_req zone=name [burst=number] [nodelay | delay=number];
limit_req zone=mylimit;
proxy_pass http://my_upstream;
}
}
limit_req_zone은 정의만 담당하며 실제 제한은 limit_req를 server나 location에 추가해야 한다.
limit_req_zone 은 3가지 매개변수를 받는다.
$binary_remote_addr로, 이진수로 이루어진 클라이언트 IP가 된다.limit_req 는 3가지 매개변수를 받을 수 있다.
Zone
Burst
Rate 값을 초과하는 Burst한 요청에 대해 수용량을 설정할 수 있다.Rate은 rate=10r/s로 1초에 10개의 요청, 즉 100ms 마다 하나의 요청을 받을 수 있는데 만약 100ms 안에 두 개의 요청이 들어올 때 두 번째 요청을 바로 거절하는 것은 사용자 경험이 좋지 않다. 그렇기 때문에 Burst한 요청의 최대값을 설정하여 해당 요청들까지는 큐에 저장해두었다가 처리하는 방식을 의미한다.burst=20 상황에서 특정 IP에 동시에 21개의 요청이 들어올 때, 하나의 요청은 바로 처리하며 나머지 20개의 요청은 큐에 담아두었다가 100ms마다 꺼내 서버로 보낸다.Delay
Rate 값에 맞춰 지연 처리한다.nodelay 옵션을 추가하면 Burst한 요청을 큐에 기다리지 않고 즉시 처리되도록 설정할 수 있다.rate값에 맞춰 큐의 슬롯이 free 상태가 된다.nodelay는 20개의 요청이 바로 처리되고 delay=3이라면 3개의 요청까지만 바로 처리되고 나머지 요청은 지연된다.rate값(여기서는 100ms)마다 하나의 슬롯씩 free 상태가 되어 요청을 수용하기 시작한다.이론으로 보면 이해하기 쉽지 않다. Nginx에서 보여주는 예제를 통해 이해해보자.
limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s;
server {
listen 80;
location / {
limit_req zone=ip burst=12 delay=8;
proxy_pass http://website;
}
}
이 예제에서 설정된 Rate Limit 정책을 살펴보자.
ip다./로 들어오는 요청에 대해 ip Rate Limit 정책을 적용한다.
그림에서 보이는 것처럼 12개의 요청까지는 거절하지 않고 수용한다. 12개의 요청 중 8개의 요청은 지연하지 않고 바로 서버로 전달하지만 8개를 초과한 요청부터 5r/s 속도를 유지하여 서버로 전달한다. 만약 12개의 요청을 초과하고 큐에 free 슬롯이 생기지 않은 상태에서 추가 요청이 들어온다면 거절된다.
Nginx에서 단순히 rate을 설정하는 것뿐만 아니라 burst나 delay를 통해 유연하게 요청을 수용할 수 있게 제공해준다.
그렇다면 프로젝트에 맞춰
burst와delay값은 어떻게 설정할까?
위에서 우리는 하나의 IP가 총 3개의 요청까지 보낼 수 있도록 임계값을 설정한 바가 있다.
rate=3r/s는 사용자가 의도적으로 발생시키는 이벤트에 대한 제한이라 생각하였다. 사용자가 버튼을 클릭하거나 입력하거나 검색하는 등 사용자의 의도가 담긴 요청에 대한 제한이라 판단하였다.
반대로 burst 요청은 사용자가 의도하지 않은 추가적인 API 요청이라 생각하였다. 예를 들어, 페이지 구성을 위한 GET 요청, 사용자 세션을 확인하는 요청 등의 요청이 포함될 수 있다.
즉, 사용자가 의도적으로 발생시키는 이벤트(rate)에 자동으로 발생하는 추가 요청(burst)이 발생하는 것이 Burst한 요청이 될 수 있을 것이라 판단하였다.
물론 개인적으로 생각한 가정일 뿐, 위의 가정과 상관없이 사용자의 의도적인 요청이 예상치 못하게 몰려서 Burst한 상황도 당연히 발생할 수 있을 것이다. (그렇기에 모니터링을 통한 조정이 필수이다..)
아무튼 위 가정을 통해 서비스에서 페이지 구성을 위해 발생하는 최대 API 요청 수를 파악해보니, 메인 페이지에서 발생하는 API 요청 수인 2회였다. 조금 더 안정적으로 Burst한 요청을 수용하기 위해 burst는 5회로 설정해주었다. 추가적으로 delay는 burst의 50% 값을 반올림하여 3으로 설정하였다.
limit_req_zone $binary_remote_addr zone=cmc_api_limit:1m rate=3r/s;
limit_req zone=cmc_api_limit burst=5 delay=3;
limit_req_status 429;
기본적으로 Nginx에서는 초과되는 요청에 대해 503(Service Temporarily Unavailable) Status 값을 반환하기 때문에 429(Too Many Requests)를 반환하도록 설정해주었다.

이제 모든 설정을 완료하였으니 테스트를 통해 제대로 요청을 제한하는 지 확인해볼 차례다.
테스트는 두 개로 분리하여 진행하였다. 하나는 '단일 IP에서 요청을 보낼 때'와 '여러 IP에서 요청을 보낼 때'다.
단일 IP 요청 제한 테스트를 통해 Nginx Rate Limit 정책이 정상적으로 적용되는 지를 확인하고, 여러 IP 요청 제한 테스트를 통해 전체적인 서버의 안정성이 유지되는 지를 확인하는 것이 목표다.
1. 단일 IP에서 요청을 보낼 때
Nginx Rate Limit 정책이 예상대로 적용되는 지 확인하기 위한 테스트다. 한 명이 무수한 요청을 보낼 때 Rate Limit 정책을 초과한 요청에 대해 429 응답 코드와 함께 거절되어야 한다.
K6 HTTP Batch를 통해 동일한 주소에 N개의 Burst 요청을 한 번에 보내는 테스트를 진행했다.
N=5
nginx-1 | 2026/05/03 06:03:54 [warn] 10#10: *47 delaying request, excess: 3.982, by zone "cmc_api_limit", client: 210.105.199.180, server: cmctv.kro.kr, request: "GET /api/battles/closed HTTP/1.1"
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 -"GET /api/battles/closed HTTP/1.1" 200
5개의 Burst 요청을 동시에 넣었을 때, 하나의 요청에 대해 Delay가 발생하는 것을 볼 수 있다. 그리고 지연된 요청에 대한 로그는 다음과 같이 나왔다.
delaying request, excess: 3.982
excess는 해당 요청이 도착했을 때 rate 기준으로 현재 얼마나 초과되었는 지를 나타내는 값이다.
위 로그에서 보여주는 excess: 3.982는 현재 요청이 도착했을 때 rate=3r/s 기준으로 약 3.982개의 초과 요청이 누적된 상태라고 판단하여, delay=3을 초과했기 때문에 해당 요청을 지연 처리시킨 것이다.
결과적으로 5개의 요청을 동시에 보냈을 때, 처음 4개의 요청은 burst=5 범위 내에서 delay=3 이하의 초과량에 해당하므로 즉시 처리되었다. 하지만 마지막 요청은 초과량이 delay 값을 넘어섰지만 여전히 burst=5 범위 안에 있었기 때문에 거부되지 않고, 허용된 처리 속도(3r/s)에 맞춰 지연된 후 처리되었다.
5개의 요청은 감질맛나니 이번에는 7개의 요청을 동시에 보내보겠다.
N=7
nginx-1 | 2026/05/03 06:16:42 [warn] 10#10: *86 delaying request, excess: 3.982, by zone "cmc_api_limit", client: 210.105.199.180, server: cmctv.kro.kr, request: "GET /api/battles/closed HTTP/1.1"
nginx-1 | 2026/05/03 06:16:42 [warn] 10#10: *85 delaying request, excess: 4.979, by zone "cmc_api_limit", client: 210.105.199.180, server: cmctv.kro.kr, request: "GET /api/battles/closed HTTP/1.1"
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 2026/05/03 06:16:42 [error] 10#10: *84 limiting requests, excess: 5.919 by zone "cmc_api_limit", client: 210.105.199.180, server: cmctv.kro.kr, request: "GET /api/battles/closed HTTP/1.1"
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 429
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
nginx-1 | 210.105.199.180 - "GET /api/battles/closed HTTP/1.1" 200
7개의 요청을 동시에 보내니 흥미로운 결과가 나왔다.
delaying request, excess: 3.982
delaying request, excess: 4.979
limiting requests, excess: 5.919
기존과 같이 excess 값이 delay=3을 초과한 값들은 delaying request 로 기록되며 지연 처리됨을 확인할 수 있었다.
반면, excess값이 burst=5를 초과한 5.919 요청에 대해서는 imiting requests 로 설정되어 429 응답이 반환되었다.
이를 통해 다음과 같은 사실을 확인할 수 있었다.
rate=3r/s는 단순히 1초에 정확히 3개의 요청만 허용한다는 의미가 아니다. Nginx는 Leaky Bucket 알고리즘을 기반으로 excess 값을 계산한다.
계산된 excess 값이 요청 처리 정책의 기준이 된다. excess가 delay 이하인 경우 요청은 즉시 처리되고, delay를 초과하지만 burst 이하인 경우에는 지연 처리되며, burst를 초과하는 경우 요청은 거부된다.
결과적으로 이번 테스트에서 excess 값에 따라 정상적으로 Rate Limit 정책에 따른 결과를 받을 수 있었다.
마지막으로 단일 IP가 악의적인 요청을 지속적으로 보냈을 때, 서버가 안정적으로 해당 요청을 거절할 수 있는 지에 대해서도 테스트를 진행하였다.
위에서 우리는 150 RPS의 요청이 들어올 때 서버는 정상적으로 응답하지 못한다는 것을 테스트했었다.
하지만 Rate Limit을 설정하였으니 이제 한 명이 150 RPS의 요청을 보내더라도 본인에게 할당된 요청 수만큼만 허용되고 나머지 요청은 거절될 테니, 서버는 안정적인 응답 속도를 보장할 수 있을 것이다.
Single IP High-RPS Protection Test Summary
------------------------------------------
Target RPS : 150
Actual RPS : 150.01
Duration : 120s
Slow threshold : 500ms
Total requests : 18001
2xx/3xx : 2.03% (365건)
429 : 97.97% (17636건)
Unexpected : 0.00% (0건)
Slow responses : 2.00% (360건)
Dropped : 0
All responses p95: 10.67ms
All responses p99: 682.45ms
429 p95 : 9.86ms
429 p99 : 12.07ms
RPS 150으로 2분간 요청을 보냈을 때, 총 18001개의 요청 중 365건만 요청을 허용하고 나머지는 거절한 것을 확인할 수 있었다.
rate=3r/s로 설정하였으니, 120초동안 약 360개의 요청을 수용할 수 있고 burst=5까지 생각하면 Rate Limit 정책에 맞게 적절히 365개의 요청을 통과시키고 있다는 것을 확인할 수 있었다.
또한 p95는 500ms 이내, p99는 1,000ms 이내의 응답 속도를 보여주기 때문에 서버는 정상적인 응답 속도를 가지고 있다고 판단하였다.
2. 여러 IP에서 요청을 보낼 때
여러 IP에서 동시에 요청을 보낼 때, 서버는 전체 RPS를 안정적으로 응답할 수 있는지를 확인하기 위한 테스트다.
우선적으로 다중 IP를 Nginx Rate Limit의 Key로 설정하기 위해서 기존 설정하였던 $binary_remote_addr를 $http_x_forwarded_for로 설정해주어야 한다.
클라이언트에서 다양한 IP로 조작하여 X-Forwarded-For 헤더에 포함하여 전달할 것이기 때문에 Nginx에서는 실제 사용자의 Client IP가 아닌 헤더로 전달된 조작 IP를 Key로 적용시켜야 한다.
위에서 우리는 최대 동시 접속자 수를 40명으로 가정하였고 이를 통해 IP별 요청 수를 3r/s로 설정하였다.
마찬가지로 다중 IP 테스트에서도 40개의 IP에서 3r/s로 요청을 보내더라도 서버는 안정적인 응답 속도를 유지해야 한다.
Multi IP Capacity Test Summary
------------------------------
IP count : 40
RPS per IP : 3
Target RPS : 120
Actual RPS : 120.00
Duration : 150s
Slow threshold : 500ms
Spoof XFF : true
Total requests : 18000
2xx/3xx : 100.00% (18000건)
429 : 0.00% (0건)
Unexpected : 0.00% (0건)
Slow responses : 0.00% (0건)
Dropped : 0
All responses p95: 73.19ms
All responses p99: 150.05ms
2xx/3xx p95 : 73.19ms
2xx/3xx p99 : 150.05ms
40명이 Rate Limit에 걸리지 않게 3r/s로 요청을 보냈을 때 예상대로 실패한 요청없이 모든 요청이 성공하였다. 또한 p95는 500ms이내에, p99는 1000ms이내의 응답 속도를 보여주어 안정적으로 응답을 처리하고 있다는 것을 확인할 수 있었다.
여기서 두 가지 다른 방식으로 더 부하를 줄 수 있다.
4r/s로 요청3r/s로 요청첫 번째 테스트부터 진행해보았다.
Multi IP Capacity Test Summary
------------------------------
IP count : 40
RPS per IP : 4
Target RPS : 160
Actual RPS : 160.00
Duration : 150s
Slow threshold : 500ms
Spoof XFF : true
Total requests : 24000
2xx/3xx : 75.83% (18200건)
429 : 24.17% (5800건)
Unexpected : 0.00% (0건)
Slow responses : 53.19% (12766건)
Dropped : 0
All responses p95: 665.34ms
All responses p99: 680.28ms
2xx/3xx p95 : 668.58ms
2xx/3xx p99 : 682.25ms
40개의 IP에서 동시에 4r/s 요청을 보내더라도 일부 요청을 거절하기 때문에 서버는 큰 무리없이 안정적인 응답 속도를 보여줄 것이라 예상하였다.
150초간 18,200건의 요청을 허용하였고 18,200/150을 하면 약 RPS 121 정도로 보아, 하나의 IP당 3r/s로 요청을 수용하고 있는 것으로 확인하였다.
p99는 1,000ms 이내의 응답속도를 보여주었지만 p95는 500ms를 초과한 응답속도를 기록하여 서버가 안정적으로 요청을 처리하고 있다고 판단할 수는 없었다.
아무래도 delay=3이 설정되어 있고 큐에는 요청이 계속 쌓이게되니, 큐에서 대기하는 시간이 중첩되어 응답이 늦어지게 된다고 추측해볼 수 있다.
다음은 50개의 IP에서 3r/s로 요청한 결과다.
Multi IP Capacity Test Summary
------------------------------
IP count : 50
RPS per IP : 3
Target RPS : 150
Actual RPS : 149.24
Duration : 150s
Slow threshold : 500ms
Spoof XFF : true
Total requests : 22386
2xx/3xx : 100.00% (22386건)
429 : 0.00% (0건)
Unexpected : 0.00% (0건)
Slow responses : 20.09% (4498건)
Dropped : 114
All responses p95: 1465.52ms
All responses p99: 1791.03ms
2xx/3xx p95 : 1465.52ms
2xx/3xx p99 : 1791.03ms
예상대로 난리가 났다.
rate=3r/s를 초과하지 않았기 때문에 모든 요청은 통과하여 보내지지만 서버가 허용할 수 있는 요청의 수를 초과하였기 때문에 현저히 느린 응답속도를 보여주게 된다.
즉, Rate Limit없이 서버는 150 RPS의 요청을 수용하게 되니 불안정한 응답속도를 보여줄 수 밖에 없다.
설명하는 내용이 많아 글의 내용이 너무 길어졌지만, 흐름만 요약한다면 다음과 같다.
1. Rate Limit Algorithm 종류
2. Rate Limit 임계값을 가정을 통해 산출하기
3. Nginx Rate Limit 설정 방법 및 적용
4. 테스트를 통해 Rate Limit 적용 여부 확인하기
그렇다면 결과적으로 "내가 설정한 Rate Limit을 통해 급증하는 서버의 트래픽에 대해 안정적으로 대응할 수 있는가?" 에 대해 대답할 수 있어야 한다. 스스로 내린 결론은 "어느 정도 그렇다"이다.
어..? 안정적으로 대응할 수 있는 것도 아니고 아닌 것도 아니고 어느 정도 그런 건 애매하게 뭐지?
잠깐 위의 테스트의 결과를 다시 가져와서 이야기를 해보겠다.
Rate Limit을 적용하면 단일 IP 또는 일부 사용자가 과도한 요청을 보낼 때, 설정한 허용량을 초과한 요청을 429로 거절할 수 있다. 실제 테스트에서도 rate=3r/s와 burst 설정에 맞게 허용 요청 수가 제한되는 것을 확인하였다. 따라서 Rate Limit은 특정 사용자의 과도한 요청이 서버 전체로 전달되는 것을 줄이는 데 효과가 있었다.
하지만 이것만으로 서버가 모든 급증 트래픽에 항상 안정적으로 대응한다고 말하기는 어렵다. 40개의 IP가 4r/s로 요청을 보낸 테스트(160 RPS)와 50개의 IP가 3r/s로 요청을 보낸 테스트 (150 RPS) 모두 안정적인 응답 기준을 만족하지 못했다.
또한 총 RPS만 보면 160 RPS 테스트가 150 RPS 테스트보다 더 느릴 것처럼 보이지만, 실제로는 160 RPS 테스트의 p95가 더 낮게 측정되며 150 RPS보다 더 빠른 응답속도를 보여주었다. 160 RPS에서는 Rate Limit 정책을 초과하여 요청을 보냈지만 150 RPS에서는 정책을 통과하여 모든 요청을 수용했기 때문에 이런 결과가 보여졌다.
이에 대해, 급증하는 트래픽에 서버가 안정적으로 반응하고 있는가에 대해 확인하기 위해서는 RPS에 집착하는 것이 아닌 Rate Limit 임계값이 적절하게 설정되었는가에 집착해야 한다는 것을 느꼈다.
그런 점에서, 이번 설정은 실제 운영 모니터링 데이터가 아니라 최대 동시 접속자 수에 대한 가정을 바탕으로 산정한 값이기 때문에 서버는 아직 급증하는 트래픽에 대해 불안정하게 처리하고 있다고 판단하였고 "어느 정도 그렇다"라고 답변한 것이다.
결과적으로 현재 설정한 Rate Limit은 과도한 요청을 제한하는 데에는 효과가 있었지만 급증하는 트래픽에 항상 안정적으로 대응하기 위해서는 실제 트래픽을 모니터링을 통해 파악하고 이를 기반으로 적절히 rate, burst, delay 값을 계속 조정해야 한다.
근데 아직 안끝났다. 글을 읽다가 조금 이상한 부분을 눈치채신 분들도 계실 것 같다.
Client IP가 한 명의 사용자를 나타내는 것은 아니잖아요. 카페 안에서 여러 사람이 동시에 요청을 보내면 어떡하죠?
Client IP가 항상 한 명의 사용자를 의미하지는 않는다. 예를 들어 같은 카페나 회사 네트워크 안에서 여러 사용자가 동시에 요청을 보내면, 서버 입장에서는 이 요청들이 모두 같은 IP에서 온 것처럼 보일 수 있다.
따라서 이 글에서 사용한 '사용자'라는 표현은 엄밀히 말하면 'Client IP'에 가깝다. 동시 접속자 수 역시 실제 사용자 수라기보다는 동시에 요청을 보내는 Client IP 수로 보는 것이 더 정확하다.
다음 글에서는 프록시 서버가 있는 환경에서 Nginx가 Client IP를 어떻게 판단하는지, 그리고 동일한 IP 뒤에 여러 사용자가 있을 때 IP 기반 Rate Limit만으로는 어떤 한계가 생기는지 다뤄볼 예정이다.
주말의 글들을 평일에 하나씩 들여다 보고 있는데요. 오늘 동훈님 글을 읽으면서
끝까지 가는 끈기를 느꼈습니다.
150 RPS(50개 IP × 3r/s) 테스트는 반전이 있었는데..
그걸 Rate Limit 임계값이 적절하게 설정되었는가에 집착해야 한다는 결론으로 귀결되어서 인상깊었습니다. ㅎㅎ
[이야기해보고 싶은 점]
nginx에 대한 이해가 없더라도 다이어그램을 통해 쉽게 이해할 수 있었는지?