이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 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버튼 비활성화, 남은 대기 시간 표시
Mobilebackground retry 제한, 사용자 알림
Partner APIquota header와 문서화된 backoff
Internal batchjob 재스케줄과 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가 필요한 이유는 무엇인가?
    • 답변: 클라이언트가 같은 의도의 요청을 다시 보낼 수 있으므로 중복 주문, 중복 결제, 중복 쿠폰 발급을 막아야 하기 때문이다.

참고 문서