HTTP 429 Too Many Requests는 서버가 일정 시간 동안 허용한 요청 수를 초과했을 때 응답하는 상태 코드입니다. 백엔드 개발을 하다 보면 외부 API 호출, 로그인 시도 제한, 알림 발송, 크롤링 방어, 결제 검증 API 연동 같은 곳에서 자주 마주치게 됩니다.
이 글에서는 HTTP 429의 의미를 먼저 정리하고, 서버와 클라이언트 양쪽에서 어떤 기준으로 대응해야 하는지 살펴보겠습니다. 단순히 재시도하면 된다는 식으로 접근하면 오히려 요청 폭주를 키울 수 있기 때문에, 실무에서는 재시도 간격, 제한 단위, 로그 설계, 사용자 안내까지 함께 보는것이 좋습니다.
HTTP 429 Too Many Requests란 무엇인가
HTTP 429 Too Many Requests는 클라이언트가 일정 시간 안에 너무 많은 요청을 보냈다는 의미의 HTTP 상태 코드입니다. 서버가 요청 자체를 이해하지 못한 것도 아니고, 인증이 실패한 것도 아니며, 현재 기준에서는 더 이상 처리하지 않겠다는 응답에 가깝습니다.
예를 들어 한 사용자가 로그인 API를 짧은 시간 안에 계속 호출하거나, 특정 IP에서 검색 API를 반복 호출하거나, 서버가 외부 결제 검증 API에 너무 자주 요청을 보내는 경우 429가 발생할 수 있습니다. 여기서 중요한 점은 “서버가 죽었다”가 아니라 “정해진 사용량을 초과했다”는 해석입니다.
HTTP 429와 503의 차이
HTTP 429와 HTTP 503은 둘 다 요청을 정상 처리하지 못한다는 점에서 비슷해 보입니다. 하지만 원인은 다릅니다. 429는 주로 클라이언트의 요청 빈도가 제한 기준을 넘었을 때 사용하고, 503은 서버가 일시적으로 요청을 처리할 수 없는 상태일 때 사용합니다.
실무에서는 이 차이를 명확히 두는 것이 좋습니다. 요청 제한 정책 때문에 거절하는 상황이라면 429가 맞고, 서버 점검이나 의존 시스템 장애처럼 서비스 자체가 일시적으로 불가능한 상황이라면 503이 더 적절합니다. 상태 코드를 섞어 쓰면 클라이언트 재시도 정책도 함께 흐려집니다.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
{
"code": "TOO_MANY_REQUESTS",
"message": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요."
}
위 예시에서 Retry-After 헤더는 클라이언트에게 몇 초 후 다시 요청할 수 있는지 알려주는 역할을 합니다. 모든 429 응답에 반드시 있어야 하는 것은 아니지만, 클라이언트가 재시도 정책을 안정적으로 만들 수 있게 하려면 가능한 한 함께 내려주는 편이 좋습니다.
HTTP 429 Too Many Requests가 발생하는 대표 상황
HTTP 429 Too Many Requests는 서비스 내부 API뿐 아니라 외부 API를 사용할 때도 자주 발생합니다. 특히 외부 플랫폼 API는 호출량 제한이 명확히 정해져 있는 경우가 많고, 제한 기준이 사용자 단위인지 앱 단위인지, 토큰 단위인지 문서마다 다릅니다.
사용자 또는 IP 단위 요청 제한
가장 흔한 방식은 사용자 ID나 IP 주소 기준으로 요청 횟수를 제한하는 것입니다. 로그인, 회원가입, 비밀번호 찾기, 인증번호 발송 API처럼 남용 가능성이 있는 기능에서 많이 사용합니다.
이 경우에는 제한 단위를 신중하게 잡아야 합니다. IP만 기준으로 제한하면 같은 회사나 학교, 공용 네트워크 사용자가 함께 영향을 받을 수 있습니다. 반대로 사용자 ID만 기준으로 제한하면 로그인 전 요청이나 익명 API에는 적용하기 어렵습니다.
외부 API 호출량 제한
백엔드 서버가 외부 API를 호출하다가 HTTP 429를 받는 경우도 많습니다. 결제 검증, 지도 API, 메시지 발송 API, 검색 API처럼 호출 비용이나 사용량 제한이 있는 서비스에서 흔히 발생합니다.
여기서 자주 하는 실수는 429가 발생했는데도 즉시 같은 요청을 반복하는 것입니다. 재시도 로직이 너무 공격적으로 동작하면 외부 API 입장에서는 더 큰 부하로 보이고, 제한 시간이 더 길어지거나 일시 차단될 수 있습니다.
배치와 스케줄러에서의 과도한 요청
배치 작업에서도 429가 발생할 수 있습니다. 예를 들어 수천 건의 데이터를 한 번에 외부 API로 검증하거나, 사용자별 알림 상태를 순회하면서 API를 호출하는 작업이 여기에 해당합니다.
배치는 사람이 직접 누르는 요청보다 빠르고 반복적입니다. 그래서 개발 환경에서는 문제가 없던 코드가 운영 데이터 규모에서 제한에 걸리는 경우가 있습니다. 이런 작업은 처음부터 처리 속도 제한, 큐잉, 재시도 간격을 함께 설계하는 것이 안전합니다.
HTTP 429 대응의 핵심은 무조건 재시도가 아니다
HTTP 429 Too Many Requests를 받았을 때 가장 먼저 떠올리는 대응은 재시도입니다. 하지만 재시도는 조건 없이 넣으면 위험합니다. 이미 요청이 많아서 거절된 상황인데, 클라이언트가 동시에 재시도하면 서버 입장에서는 요청이 더 몰리는 구조가 됩니다.
따라서 429 대응은 “언제 다시 보낼 것인가”, “몇 번까지 보낼 것인가”, “실패를 사용자에게 어떻게 안내할 것인가”를 함께 정해야 합니다. 이 기준이 없으면 장애가 아니던 상황도 장애처럼 번질 수 있습니다.
Retry-After 헤더를 우선 확인한다
서버가 Retry-After 헤더를 내려준다면 클라이언트는 이 값을 우선 기준으로 삼는 것이 좋습니다. 서버가 직접 “이 시간 이후에 다시 요청하라”고 알려주는 정보이기 때문입니다.
Retry-After: 30
위 값은 30초 후에 다시 요청하라는 의미입니다. 클라이언트가 이 값을 무시하고 1초마다 계속 호출하면 서버의 제한 정책과 충돌하게 됩니다. 외부 API를 사용하는 서버 코드에서도 동일하게 적용됩니다.
Exponential Backoff를 적용한다
Retry-After가 없거나, 내부 정책상 직접 재시도 간격을 정해야 한다면 Exponential Backoff 방식을 사용할 수 있습니다. 실패할 때마다 재시도 간격을 점점 늘리는 방식입니다.
예를 들어 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후처럼 간격을 늘립니다. 여기에 작은 랜덤 지연값을 더하면 여러 클라이언트가 동시에 다시 요청하는 문제를 줄일 수 있습니다.
function getRetryDelay(attempt: number): number {
const baseDelay = 1000;
const maxDelay = 30000;
const jitter = Math.floor(Math.random() * 500);
return Math.min(baseDelay * Math.pow(2, attempt), maxDelay) + jitter;
}
재시도 간격에는 최대값을 두는 편이 좋습니다. 간격이 무한정 늘어나면 사용자 입장에서는 요청이 멈춘 것처럼 보일 수 있고, 서버 입장에서도 오래된 작업이 뒤늦게 실행되어 예상하지 못한 흐름을 만들 수 있습니다.
서버에서 HTTP 429를 설계하는 방법
HTTP 429 Too Many Requests는 클라이언트가 받는 응답이지만, 좋은 429 대응은 서버 설계에서 시작됩니다. 제한 기준이 명확하지 않으면 클라이언트는 왜 막혔는지 알기 어렵고, 운영자는 어떤 요청이 문제인지 추적하기 어렵습니다.
제한 단위를 먼저 정한다
Rate Limit을 만들 때는 무엇을 기준으로 제한할지 먼저 정해야 합니다. 사용자 ID, IP, API Key, 디바이스 ID, 세션, 엔드포인트 등 선택지가 있습니다. 모든 API에 같은 기준을 적용하기보다는 기능의 성격에 따라 나누는 것이 좋습니다.
| 제한 기준 | 적합한 상황 | 주의할 점 |
|---|---|---|
| IP | 비로그인 요청, 공개 API | 공용 네트워크 사용자가 함께 제한될 수 있음 |
| 사용자 ID | 로그인 이후 API | 로그인 전 요청에는 적용하기 어려움 |
| API Key | 파트너 API, 외부 연동 | 키 공유나 유출 시 추적 기준이 흐려질 수 있음 |
| 엔드포인트 | 비용이 큰 API, 민감 기능 | 공통 정책보다 세밀한 관리가 필요함 |
실무에서는 하나의 기준만 쓰기보다 여러 기준을 조합하는 경우가 많습니다. 예를 들어 로그인 API는 IP와 계정 식별자를 함께 보고, 외부 파트너 API는 API Key와 엔드포인트를 함께 보는 방식입니다.
응답 메시지는 짧고 명확하게 작성한다
429 응답에는 내부 정책을 모두 노출할 필요가 없습니다. 다만 클라이언트가 다음 행동을 판단할 수 있는 정도의 정보는 제공해야 합니다. 사용자에게 보여줄 메시지와 개발자가 확인할 에러 코드를 분리하는 방식이 관리하기 쉽습니다.
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.",
"retryAfterSeconds": 60
}
내부적으로는 어떤 제한 정책에 걸렸는지 로그에 남기고, 응답에는 필요한 만큼만 내려주는 편이 좋습니다. 제한 기준을 지나치게 자세히 노출하면 우회 시도에 힌트를 줄 수 있습니다.
Spring Boot에서 HTTP 429 응답 처리 예시
HTTP 429 Too Many Requests를 Spring Boot에서 처리할 때는 예외를 명확히 분리하고, 공통 응답 형식을 맞추는 것이 좋습니다. 컨트롤러마다 직접 상태 코드를 내려주기보다 예외와 전역 핸들러를 두면 유지보수하기 쉽습니다.
RateLimitExceededException 정의
public class RateLimitExceededException extends RuntimeException {
private final long retryAfterSeconds;
public RateLimitExceededException(long retryAfterSeconds) {
super("Too many requests");
this.retryAfterSeconds = retryAfterSeconds;
}
public long getRetryAfterSeconds() {
return retryAfterSeconds;
}
}
예외 안에 retryAfterSeconds를 포함하면 전역 예외 처리기에서 Retry-After 헤더와 응답 바디를 함께 구성할 수 있습니다. 예외 메시지 문자열을 파싱하는 방식은 나중에 변경에 약해지기 때문에 권장하지 않습니다.
전역 예외 처리에서 429 응답 내려주기
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RateLimitExceededException.class)
public ResponseEntity<ErrorResponse> handleRateLimitExceeded(
RateLimitExceededException e
) {
ErrorResponse body = new ErrorResponse(
"RATE_LIMIT_EXCEEDED",
"요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.",
e.getRetryAfterSeconds()
);
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.header(HttpHeaders.RETRY_AFTER, String.valueOf(e.getRetryAfterSeconds()))
.body(body);
}
}
public record ErrorResponse(
String code,
String message,
long retryAfterSeconds
) {
}
이렇게 처리하면 API 응답 형식을 일정하게 유지할 수 있습니다. 프론트엔드나 앱에서는 code 값을 기준으로 안내 문구를 분기하고, retryAfterSeconds 값을 이용해 버튼 비활성화나 재요청 시점을 제어할 수 있습니다.
클라이언트에서 HTTP 429를 다루는 방식
HTTP 429 Too Many Requests는 서버만의 문제가 아닙니다. 클라이언트가 어떻게 반응하느냐에 따라 사용자 경험이 좋아질 수도 있고, 반대로 같은 요청을 계속 반복하는 문제가 생길 수도 있습니다.
사용자 액션은 잠시 막아야 한다
사용자가 버튼을 반복해서 누르는 상황이라면 429 응답 이후 일정 시간 동안 버튼을 비활성화하는 것이 좋습니다. 특히 인증번호 발송, 쿠폰 발급, 결제 요청처럼 중복 요청이 민감한 기능에서는 클라이언트 제어가 도움이 됩니다.
다만 클라이언트 제어만 믿어서는 안 됩니다. 브라우저 개발자 도구나 직접 호출을 통해 우회할 수 있기 때문에 최종 제한은 서버에서 처리해야 합니다. 클라이언트 제어는 사용자 경험을 좋게 만드는 보조 장치로 보는 것이 맞습니다.
자동 재시도는 요청 성격에 따라 다르게 적용한다
모든 요청을 자동으로 재시도하면 안 됩니다. 단순 조회 요청은 일정 조건에서 재시도할 수 있지만, 결제 승인, 포인트 차감, 주문 생성처럼 상태를 변경하는 요청은 훨씬 조심해야 합니다.
상태 변경 요청에서는 멱등성 키를 함께 설계하는 편이 좋습니다. 같은 요청이 여러 번 들어와도 한 번만 처리되도록 서버가 보장해야 클라이언트 재시도도 안전해집니다.
POST /orders
Idempotency-Key: 8f2b1c7e-9b3a-4c1a-9f3a-1f2b3c4d5e6f
{
"productId": 1001,
"quantity": 1
}
이런 구조가 없다면 클라이언트는 “재시도해도 되는 요청인지” 판단하기 어렵습니다. 팀에서 API를 설계할 때 조회 요청과 변경 요청의 재시도 정책을 분리해 문서화해 두면 협업할 때 혼선이 줄어듭니다.
HTTP 429 로그와 모니터링 기준
HTTP 429 Too Many Requests는 정상적인 제한 정책의 결과일 수도 있고, 잘못된 클라이언트 구현의 신호일 수도 있습니다. 그래서 단순히 에러 로그로만 쌓기보다는 어떤 기준에서 제한되었는지 함께 남기는 것이 중요합니다.
로그에는 제한 정책 정보를 남긴다
429 로그에는 요청 경로, 제한 기준, 식별자, 제한 정책 이름, 재시도 가능 시간 정도를 남기면 분석에 도움이 됩니다. 단, 개인정보나 민감한 토큰을 그대로 남기면 안 됩니다.
{
"event": "rate_limit_exceeded",
"path": "/api/auth/sms",
"policy": "sms-send-per-user",
"keyType": "userId",
"retryAfterSeconds": 60
}
이 정도 정보가 있으면 특정 API에서 제한이 자주 발생하는지, 특정 정책이 너무 빡빡한지, 클라이언트가 재시도를 잘못하고 있는지 확인할 수 있습니다. 운영 중에는 429 자체보다 429의 패턴을 보는 것이 더 중요할 때가 많습니다.
모든 429를 장애로 볼 필요는 없다
Rate Limit은 의도적으로 요청을 막는 정책입니다. 따라서 429가 발생했다고 해서 모두 장애로 분류할 필요는 없습니다. 로그인 공격 방어, 인증번호 발송 제한, 외부 API 보호처럼 정상적으로 동작한 결과일 수 있습니다.
다만 평소보다 429 비율이 갑자기 높아졌다면 원인을 봐야 합니다. 앱 배포 후 동일 요청을 반복 호출하는 버그가 생겼거나, 배치 작업이 제한 기준을 고려하지 않고 실행되었거나, 외부 API 정책이 변경되었을 수 있습니다.
HTTP 429 대응에서 자주 하는 실수
HTTP 429 Too Many Requests 대응은 코드 몇 줄로 끝나는 문제가 아닙니다. 서버 정책, 클라이언트 동작, 외부 API 문서, 사용자 안내가 맞물려야 합니다. 아래 실수들은 실제 개발 과정에서 자주 보입니다.
즉시 재시도를 반복한다
가장 피해야 할 방식은 429를 받자마자 바로 재시도하는 것입니다. 특히 여러 인스턴스나 여러 사용자 요청이 동시에 같은 로직을 타면 짧은 시간 안에 요청이 더 몰릴 수 있습니다.
재시도에는 반드시 간격과 최대 횟수가 있어야 합니다. Retry-After가 있으면 우선 적용하고, 없으면 Exponential Backoff와 Jitter를 조합하는 방식이 무난합니다.
429를 일반 서버 에러처럼 처리한다
429를 500 계열 에러처럼 처리하면 원인 분석이 어려워집니다. 429는 제한 정책에 따른 응답이므로, 일반 예외 알림과 같은 레벨로 묶으면 불필요한 알림이 많아질 수 있습니다.
대신 제한 정책별 발생량과 비율을 따로 보는 것이 좋습니다. 특정 정책에서만 급증하는지, 특정 API에서만 발생하는지 구분해야 실제 원인에 가까워질 수 있습니다.
사용자에게 원인을 설명하지 않는다
사용자 입장에서 버튼을 눌렀는데 아무 반응이 없거나 “오류가 발생했습니다”만 보이면 같은 행동을 반복하게 됩니다. 이 경우 클라이언트 요청이 더 늘어날 수 있습니다.
“잠시 후 다시 시도해 주세요”, “1분 후 다시 요청할 수 있습니다”처럼 짧고 명확한 안내가 필요합니다. 보안상 자세한 제한 기준을 알려줄 필요는 없지만, 사용자가 다음 행동을 이해할 정도의 안내는 제공해야 합니다.
HTTP 429 대응 전략 정리
HTTP 429 Too Many Requests는 요청 제한 정책이 동작하고 있다는 신호입니다. 따라서 단순 에러로만 보기보다, 서버와 클라이언트 사이의 요청 제어 계약으로 이해하는 편이 좋습니다.
서버는 제한 기준과 응답 형식을 명확히 해야 합니다. 가능하면 Retry-After 헤더를 제공하고, 로그에는 어떤 정책에 의해 제한되었는지 남겨야 합니다. 클라이언트는 이 정보를 바탕으로 재시도 간격을 조절하고, 사용자에게 적절한 안내를 제공해야 합니다.
실무 기준으로 보면 좋은 429 대응은 세 가지로 정리할 수 있습니다. 무조건 재시도하지 않는 것, 제한 기준을 기능별로 분리하는 것, 그리고 429 발생 패턴을 관찰할 수 있게 만드는 것입니다. 이 세 가지가 갖춰지면 HTTP 429는 막연한 오류가 아니라 서비스 보호를 위한 제어 장치로 다룰 수 있습니다.
정리하면 다음 기준으로 접근하면 됩니다.
- HTTP 429는 요청 제한 초과를 의미한다.
- Retry-After가 있으면 클라이언트는 해당 값을 우선 따른다.
- 자동 재시도에는 최대 횟수와 지연 간격이 필요하다.
- 상태 변경 요청은 멱등성까지 함께 설계해야 한다.
- 429 로그는 정책별로 분리해서 관찰하는 것이 좋다.
'개발 > 기타' 카테고리의 다른 글
| HTTP Read timed out 문제 추적 방법: 어디서 시간이 멈췄는지 확인하는 실무 기준 (0) | 2026.05.31 |
|---|---|
| 배치 프로세싱(Batch API)을 활용한 비실시간 작업 비용 최적화 (0) | 2026.03.24 |
| Git과 GitHub 차이, 왜 필요한가? (0) | 2026.02.10 |
| 개발자에게 추천하는 AI 도구, 생산성을 높이는 선택 기준 (0) | 2026.01.20 |
| AWS 비용 폭탄 막는 방법, 초보자가 반드시 알아야 할 관리 전략 (0) | 2026.01.19 |
