인사이트

인사이트

레이트 리미팅 실전: 토큰 버킷·429·Retry-After로 API를 급증 트래픽에서 지키기

레이트 리미팅 실전 토큰 버킷·429·Retry-After로 API를 급증 트래픽에서 지키기

🤖 AI Summary

API는 언젠가 예상보다 많은 요청을 받습니다. 문제는 한 클라이언트의 폭주가 모두의 장애가 된다는 점이죠. 그래서 레이트 리미팅(요청 속도 제한)이 필요합니다. 구현의 뼈대는 두 알고리즘입니다. 토큰 버킷은 일정 속도로 채워지는 토큰을 요청이 소모하는 방식으로 순간 폭증(burst)을 허용하며, AWS API Gateway가 이 방식을 씁니다. 리키 버킷은 요청을 일정 속도로 흘려보내 처리량을 평활화하며, NGINX의 limit_req가 이 방식입니다. 거절할 때의 규약은 RFC 6585의 429 Too Many Requests와 Retry-After 헤더입니다. 함정도 있습니다. NGINX의 거절 기본 상태 코드는 429가 아니라 503이라 별도 설정이 필요하고, 클라이언트가 즉시 재시도를 반복하면 차단은 오히려 길어집니다. 이 글은 알고리즘 선택부터 응답 설계, 실무 설정까지 1차 공식 문서로 정리합니다.

블로그 목차

한 클라이언트의 폭주가 모두의 장애가 되기 전에

트래픽 급증은 공격만의 이야기가 아닙니다. 버그로 무한 재시도에 빠진 클라이언트, 갑자기 유명해진 이벤트 페이지, 잘못 짠 배치 스크립트 하나면 충분합니다. 제한이 없으면 가장 시끄러운 클라이언트가 서버 자원을 독식하고, 나머지 정상 사용자들이 함께 느려지거나 죽습니다. 레이트 리미팅은 이 공유 자원의 공정성과 생존을 지키는 최소한의 장치입니다.

흥미로운 점은, 표준이 "무엇을 기준으로 얼마나 제한하라"고 정해주지 않는다는 것입니다. 429를 정의한 RFC 6585는 "서버가 사용자를 어떻게 식별하고 요청을 어떻게 세는지는 이 규격이 정의하지 않는다"고 명시합니다. 즉 IP 단위로 셀지, API 키 단위로 셀지, 계정 단위로 셀지는 설계자의 몫입니다. 규격이 정해 주는 것은 "거절할 때 어떻게 말할 것인가"뿐이고, 어떻게 셀 것인가는 알고리즘의 영역입니다. 그 알고리즘부터 보겠습니다.




두 개의 버킷: 토큰 버킷과 리키 버킷

레이트 리미팅 구현의 뼈대는 버킷(양동이) 비유를 쓰는 두 알고리즘입니다. 이름은 비슷하지만 동작이 다릅니다.

토큰 버킷(token bucket)은 버킷에 일정 속도로 토큰이 채워지고, 요청 하나가 토큰 하나를 소모합니다. 토큰이 남아 있으면 요청은 즉시 처리되므로, 버킷 용량만큼의 순간 폭증을 허용하면서 평균 속도를 제한합니다. AWS API Gateway가 정확히 이 방식입니다. 공식 문서 기준으로 API Gateway는 "토큰 하나가 요청 하나로 계산되는 토큰 버킷 알고리즘"으로 스로틀링하며, 사용량 계획에서 rate는 토큰이 버킷에 추가되는 속도, burst는 버킷의 용량을 뜻합니다. 한도를 넘으면 클라이언트는 429 응답을 받을 수 있습니다. 문서는 이 한도가 보장된 상한이 아니라 최선 노력(best-effort) 기준의 목표치라는 점도 명시합니다.

리키 버킷(leaky bucket)은 반대로 요청이 버킷에 쌓이고, 바닥의 구멍으로 일정 속도로만 빠져나갑니다. 순간적으로 몰려온 요청도 처리 속도는 일정하게 유지되므로, 처리량을 평활화하는 효과가 있습니다. NGINX의 limit_req 모듈이 이 방식입니다. 공식 문서는 이 모듈을 "정의된 키(대표적으로 단일 IP 주소)별 요청 처리 속도를 제한"하는 모듈로 소개하고, 그 제한이 "리키 버킷 방식"으로 이뤄진다고 별도로 밝힙니다. 넘치는 요청은 곧바로 버려지는 게 아니라 지연되다가, 최대 burst 크기를 넘으면 에러로 종료됩니다.

두 개의 버킷

거칠게 요약하면 이렇습니다. 순간 폭증을 어느 정도 받아주면서 평균을 지키고 싶다면 토큰 버킷, 뒷단(백엔드·DB)이 감당할 처리 속도를 일정하게 유지하고 싶다면 리키 버킷이 자연스럽습니다. 실무에서는 게이트웨이·프록시가 이미 하나를 구현하고 있으므로, 내가 쓰는 도구가 어느 쪽인지 아는 것이 먼저입니다.




거절의 규약: 429와 Retry-After

제한에 걸린 요청을 어떻게 거절할 것인가. 여기는 표준이 있습니다. RFC 6585가 정의한 429 Too Many Requests입니다. 정의는 간명합니다. "사용자가 주어진 시간 동안 너무 많은 요청을 보냈다('레이트 리미팅')"는 뜻입니다. 그리고 응답에 대해 두 가지를 말합니다. 상황을 설명하는 내용을 포함해야 하고(SHOULD), 언제 다시 시도하면 되는지 알려주는 Retry-After 헤더를 포함할 수 있습니다(MAY). 하나 더 있습니다. RFC는 429 응답을 캐시에 저장해서는 안 된다(MUST NOT)고 못 박습니다. 이 조항을 지키면, CDN이나 프록시 뒤에서 특정 클라이언트에게 내린 429가 캐시돼 모든 사용자에게 서빙되는 사고를 막을 수 있습니다.

Retry-After의 형식은 두 가지입니다. HTTP 날짜 또는 초 단위 지연 시간(음이 아닌 정수). MDN 기준으로 이 헤더는 429뿐 아니라 503(서비스 일시 불가), 301 같은 리다이렉트에서도 쓰입니다. 헤더 하나로 클라이언트에게 "언제 오라"를 말해줄 수 있으니, MAY라 해도 실무에서는 넣는 쪽이 서로에게 이롭습니다. 클라이언트의 무의미한 재시도 폭주 자체를 줄여 주기 때문이죠.

클라이언트 쪽 예절도 규약의 일부입니다. AWS 문서는 429를 받으면 "클라이언트가 실패한 요청을 속도를 제한하는 방식으로 다시 제출할 수 있다"고 안내합니다. 즉시 재시도를 반복하는 클라이언트는 토큰이 채워질 틈, 버킷이 빌 틈을 주지 않아 차단 상태를 스스로 연장합니다. Retry-After를 존중하고, 없다면 간격을 점점 늘리는 백오프가 안전합니다.

거절과 재시도의 규약




실무 설정과 함정

개념을 알았으니 설정으로 내려가 보겠습니다. NGINX 기준으로 최소 구성은 두 줄입니다. limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;로 IP별 상태를 저장할 존과 평균 속도를 정의하고, 적용할 위치에 limit_req zone=one burst=5;를 둡니다. 공식 문서의 설명대로 이 조합은 평균 초당 1건, 폭증은 5건까지 허용하는 제한이 됩니다. 지연 없이 폭증분을 즉시 처리하고 싶다면 nodelay를, 일부까지만 즉시 처리하고 나머지는 지연시키고 싶다면 delay= 파라미터(NGINX 1.15.7 이상)를 씁니다.

여기서 함정 세 가지를 짚습니다.

  • 함정 1: NGINX의 거절 기본값은 503입니다. 레이트 리미팅으로 거절했는데 클라이언트는 "서버 장애"로 읽는 상황이 생깁니다. limit_req_status 429;로 의미에 맞는 코드를 돌려주는 것이 좋습니다.

  • 함정 2: burst를 0으로 두면(기본값) 평균을 조금만 넘어도 바로 거절됩니다. 정상 사용자의 자연스러운 클릭 연타까지 걸릴 수 있으니, 트래픽 패턴을 보고 burst를 현실적으로 잡아야 합니다. limit_req_dry_run(NGINX 1.17.1 이상)을 켜면 실제로 거절하지 않고 초과 요청을 집계만 하므로, 실트래픽으로 안전하게 튜닝할 수 있습니다.

  • 함정 3: 한도는 절대 방어선이 아닙니다. AWS 문서는 스로틀·쿼터를 "보장된 상한이 아니라 최선 노력 기준의 목표치"로 명시합니다. 여기에 RFC 6585의 보안 고려사항도 같은 결론을 가리킵니다. 공격 상황에서는 429로 일일이 응답하는 것 자체가 자원을 소모하므로 서버가 429를 쓸 의무는 없고, 연결을 그냥 끊는 편이 더 적절할 수 있다고 밝힙니다. 레이트 리미팅은 공정성 장치이지, 대규모 공격의 방패가 아닙니다. 초당 수억 건 규모의 봇넷 트래픽은 원점이 아니라 엣지에서 흡수해야 합니다. 그 규모의 이야기는 Aisuru·Kimwolf 봇넷과 205Mrps 글에서 다뤘습니다.

마지막으로 기준 선택. 앞서 본 대로 규격은 식별·카운트 방법을 정하지 않으므로, 로그인 서비스는 계정 단위, 공개 API는 API 키 단위, 익명 트래픽은 IP 단위가 일반적인 출발점입니다. NGINX는 키 기반(대표적으로 IP), AWS API Gateway는 사용량 계획과 API 키로 클라이언트별 한도를 걸며, 사용량 계획이 없어도 계정·리전 단위 기본 한도는 항상 적용됩니다. 어디를 기준으로 세든, 거절은 429로, 안내는 Retry-After로. 이 두 가지만 지켜도 클라이언트와 모니터링이 상황을 오해하지 않습니다.




이것만 기억하세요

레이트 리미팅의 뼈대는 토큰 버킷(폭증 허용 + 평균 제한, AWS)과 리키 버킷(처리 속도 평활화, NGINX)입니다. 거절할 때는 429 Too Many Requests + Retry-After가 규약입니다(RFC 6585). 특히 NGINX 거절 기본값은 503이라 limit_req_status 429 설정이 필요하고, 한도는 보장 상한이 아닌 best-effort 목표치라 대규모 공격 방어는 별도 층(엣지)의 몫입니다.




자주 묻는 질문 (FAQ)

Q. 요청을 거절할 때 429와 503 중 무엇을 반환해야 하나요?

레이트 리미팅 거절이라면 429가 규격에 맞습니다. RFC 6585가 429를 "주어진 시간 동안 너무 많은 요청(레이트 리미팅)"으로 정의해요. 주의할 점은 NGINX limit_req의 거절 기본값이 503이라는 것입니다. limit_req_status로 429로 바꿔 주는 편이 의미상 정확합니다. 503은 서버 장애로 읽혀 클라이언트와 모니터링이 원인을 오해할 수 있어요.

Q. Retry-After 헤더는 반드시 보내야 하나요?

필수는 아닙니다. RFC 6585 기준으로 429 응답은 상황 설명을 포함해야 하고(SHOULD), Retry-After는 포함할 수 있습니다(MAY). 다만 언제 다시 오라고 알려주면 무의미한 재시도 폭주가 줄어 실무에서는 넣는 편이 좋아요. 값은 HTTP 날짜 또는 초 단위 지연(음이 아닌 정수) 두 형식입니다.

Q. 토큰 버킷과 리키 버킷은 무엇이 다른가요?

토큰 버킷은 일정 속도로 채워지는 토큰을 요청이 소모하는 방식으로, 버킷 용량(burst)만큼 순간 폭증을 허용하면서 평균을 제한합니다. AWS API Gateway가 이 방식이에요. 리키 버킷은 요청이 쌓이고 일정 속도로만 빠져나가 처리량을 평활화하는 방식으로, NGINX limit_req가 이 방식입니다. 초과 요청은 지연되다가 burst를 넘으면 에러로 종료돼요.

Q. 누구를 기준으로 제한해야 하나요? IP? API 키?

규격에 정답은 없습니다. RFC 6585는 사용자 식별과 요청 카운트 방법을 정의하지 않는다고 명시해요. 그래서 설계자의 몫입니다. 로그인 서비스는 계정 단위, 공개 API는 API 키 단위, 익명 트래픽은 IP 단위가 일반적인 출발점입니다. NGINX는 키(대표적으로 IP) 기반, AWS는 사용량 계획과 API 키 기반이에요.

Q. 클라이언트 쪽에서는 429를 어떻게 처리해야 하나요?

오류로 흘려보내지 말고 재시도 신호로 다뤄야 합니다. Retry-After가 있으면 그만큼 기다렸다 재시도하고, 없으면 지수 백오프처럼 간격을 늘려 갑니다. AWS 문서도 429를 받으면 속도를 제한하는 방식으로 다시 제출할 수 있다고 안내해요. 즉시 재시도를 반복하면 차단이 오히려 길어집니다.

비용 절감부터 차별화된 속도와 안정적 운영까지
기업에 최적화된 IT 환경을 지원합니다

비용 절감부터 차별화된 속도와
안정적 운영까지 기업에 최적화된 IT 환경을 지원합니다

비용 절감부터
차별화된 속도와 안정적 운영까지
기업에 최적화된 IT 환경을 지원합니다

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호 (위례 더존메디컬타워)

TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호
(위례 더존메디컬타워)


TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호
(위례 더존메디컬타워)


TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved