이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 429 응답은 단순 오류가 아니라 어떤 클라이언트 계약인가?
Retry-After, quota header, error body는 재시도 폭주와 UX에 어떤 영향을 주는가?- 서버와 클라이언트가 제한 상황에서 어떤 행동을 약속해야 하는가?
개요
429 Too Many Requests는 “실패했으니 알아서 다시 해라”가 아니다. 서버가 지금 요청을 받을 수 없거나 정책상 허용하지 않으며, 클라이언트가 언제 어떻게 다시 시도해야 하는지 알려주는 계약이다.
429 계약이 없으면 클라이언트는 즉시 재시도할 수 있다. 그러면 rate limit이 부하를 줄이는 장치가 아니라 retry storm을 만드는 신호가 된다.
따라서 429 설계는 HTTP status, header, body, 클라이언트 retry 정책, 사용자 문구까지 포함해야 한다.
429가 바꾸는 것
| 축 | 영향 |
|---|---|
| 성능 | 서버가 감당 불가능한 요청을 일찍 거절해 핵심 경로를 보호한다 |
| 정합성 | 쓰기 중복과 불완전한 처리 진입을 줄일 수 있다 |
| 장애 격리 | 특정 사용자나 endpoint 폭주를 전체 장애로 번지기 전에 자른다 |
| 운영 복잡도 | 클라이언트별 retry, header, 모니터링, 고객 안내가 필요해진다 |
429는 정상적인 제어 응답이다. 하지만 사용자는 실패로 느낄 수 있으므로 UX와 API 문서가 함께 있어야 한다.
최소 응답
가장 단순한 429 응답은 다음 정보를 포함한다.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json{
"code": "RATE_LIMITED",
"message": "요청이 많습니다. 30초 후 다시 시도해 주세요.",
"retryAfterSeconds": 30,
"requestId": "req-20260701-0001"
}Retry-After는 클라이언트의 즉시 재시도를 막는다. requestId는 고객 문의와 로그 추적을 돕는다.
Quota Header
Public API나 partner API는 남은 quota 정보를 제공하는 것이 유용하다.
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30이 정보는 클라이언트가 자기 호출량을 조절하게 만든다. 다만 일반 사용자 화면에는 quota 숫자를 노출하는 것보다 이해 가능한 안내가 더 중요할 수 있다.
Header는 알고리즘과 맞아야 한다. Token Bucket에서 reset은 다음 token이 생기는 시간에 가깝고, Fixed Window에서는 window reset까지의 시간에 가깝다.
Retry 정책
클라이언트는 429를 받았을 때 즉시 재시도하면 안 된다.
1. Retry-After가 있으면 그 시간까지 기다린다.
2. Retry-After가 없으면 exponential backoff와 jitter를 적용한다.
3. 같은 쓰기 요청은 idempotency key를 유지한다.
4. 최대 재시도 횟수를 넘으면 사용자에게 상태를 보여준다.서버가 429를 주면서 Retry-After를 빼면 클라이언트마다 다른 추측을 하게 된다. 일부 SDK는 짧은 backoff로 계속 재시도해 장애를 키울 수 있다.
429와 503 구분
429와 503은 모두 재시도 가능해 보일 수 있지만 의미가 다르다.
| 상태 | 의미 | 예시 |
|---|---|---|
| 429 | 특정 주체나 정책이 quota를 초과했다 | userId 60/min 초과 |
| 503 | 서비스가 일시적으로 처리할 수 없다 | 전체 DB 장애, 배포 중 |
전체 시스템 과부하에서 모든 사용자에게 거절을 주는 경우 503 + Retry-After가 더 적합할 수 있다. 특정 사용자나 API key의 quota 초과라면 429가 맞다.
쓰기 요청과 Idempotency
쓰기 요청에서 429 이후 재시도는 중복 위험을 만든다. 주문 생성, 결제, 쿠폰 발급은 클라이언트가 같은 요청을 다시 보낼 수 있으므로 idempotency key가 필요하다.
POST /orders
Idempotency-Key: ord-create-79f서버는 같은 key의 재시도에 대해 새 주문을 만들지 않고 이전 결과를 반환하거나 처리 중 상태를 반환해야 한다.
{
"code": "REQUEST_PENDING",
"message": "이전 요청을 처리 중입니다.",
"statusUrl": "/orders/requests/ord-create-79f"
}이 계약이 없으면 rate limit이 중복 생성 방어로 오해될 수 있다. Rate Limit은 속도 제한이고, idempotency는 같은 의도의 중복 실행 방어다.
UX 문구
사용자에게는 정책 이름보다 행동이 중요하다.
나쁜 문구:
Too Many Requests좋은 문구:
요청이 많아 잠시 대기 중입니다.
30초 후 다시 시도해 주세요.
결제는 아직 진행되지 않았습니다.특히 결제, 예약, 쿠폰 발급처럼 사용자가 결과를 불안해하는 기능은 “처리됐는지”를 명확히 알려야 한다.
클라이언트별 계약
| 클라이언트 | 계약 |
|---|---|
| Web | 버튼 비활성화, 남은 대기 시간 표시 |
| Mobile | background retry 제한, 사용자 알림 |
| Partner API | quota header와 문서화된 backoff |
| Internal batch | job 재스케줄과 priority 낮춤 |
서버만 제한해도 충분하지 않다. 클라이언트가 제한 신호를 이해해야 전체 부하가 줄어든다.
관측 지표
429를 운영할 때는 다음을 본다.
- 429 rate by endpoint
- 429 rate by user/tenant/API key
- retry-after 분포
- 429 이후 재시도율
- 제한으로 보호된 downstream latency
- 사용자 전환율과 이탈률
- support ticket 수
429 rate가 높아졌다고 무조건 좋은 것은 아니다. bot을 막은 것일 수도 있지만 정상 사용자를 막고 있을 수도 있다.
위험 신호
- 429 body가 없고 클라이언트가 어떻게 행동해야 할지 모른다.
Retry-After없이 429를 반환한다.- 모든 과부하를 429로 표현해 시스템 장애와 quota 초과를 구분하지 못한다.
- 쓰기 요청 재시도에 idempotency key가 없다.
- 프론트엔드가 429를 일반 500 오류처럼 보여준다.
- partner API 문서에 quota와 reset 기준이 없다.
개인 프로젝트 기준
- 429 응답에
Retry-After와 JSON body를 넣는다. - 클라이언트가 즉시 재시도하지 않는 예시를 남긴다.
- 쓰기 API에는 idempotency key 필요 여부를 적는다.
- 429 로그에 key, endpoint, retryAfter, requestId를 남긴다.
기업 운영 기준
- 429, 503, 409 같은 상태 코드 사용 기준을 API guideline에 둔다.
- SDK나 client library가
Retry-After와 jitter를 지키게 한다. - partner별 quota 문서와 dashboard를 제공한다.
- 정책 변경 후 정상 사용자 피해와 downstream 보호 효과를 함께 검증한다.
확인 질문
확인 질문
- 확인 질문: 429가 단순 오류가 아닌 이유는 무엇인가?
- 답변: 서버가 요청을 거절하는 조건과 클라이언트의 다음 행동, 재시도 시점을 알려주는 API 계약이기 때문이다.
- 확인 질문: 429와 503을 구분해야 하는 이유는 무엇인가?
- 답변: 429는 특정 주체의 quota 초과이고 503은 서비스 전체가 처리 불가능한 상태에 가까워 클라이언트 대응과 운영 해석이 다르기 때문이다.
- 확인 질문: 쓰기 요청에서 429 이후 재시도에 idempotency key가 필요한 이유는 무엇인가?
- 답변: 클라이언트가 같은 의도의 요청을 다시 보낼 수 있으므로 중복 주문, 중복 결제, 중복 쿠폰 발급을 막아야 하기 때문이다.