사전 HTTP 429
에러코드

HTTP 429

gabury1

429 는 서버가 돌려주는 상태 코드입니다. 요청을 너무 많이 보냈다는 뜻입니다. 몇 번까지 받아줄지는 서버가 정합니다.

쉽고 빠른 이해

429 는 서버가 "요청을 너무 많이 보냈다"고 돌려주는 상태 코드입니다. GitHub 은 요청이 한도를 넘으면 이 코드를 돌려줄 수 있습니다.

한도를 넘겨도 서버가 아무 신호를 안 주면 클라이언트는 계속 요청을 밀어붙입니다. 429 는 "그만 보내고 기다리라"는 신호를 명시적으로 돌려줘서 이걸 막습니다.

  1. 서버가 요청을 무엇 기준(주소·계정 등)으로 셀지 정하고, 한도를 넘으면 429 를 돌려줍니다
  2. 응답에 언제 다시 요청해도 되는지 알리는 값을 함께 실어 보낼 수 있습니다
  3. 그 값이 없으면 클라이언트가 스스로 기다릴 시간을 정해 다시 시도합니다

다만 그 값을 싣는 방식이 표준으로 하나만 정해진 게 아니라, 서비스마다 다른 이름의 헤더를 붙입니다.

429 를 쓰는 데도 대가가 있습니다. 공격을 받거나 요청이 한꺼번에 몰릴 때 하나하나에 429 로 응답하면 그 자체가 서버 자원을 씁니다. 그래서 429 대신 그냥 연결을 끊는 서버도 있습니다.

상세

429 는 4xx 에 속합니다. IETF(Internet Engineering Task Force, 인터넷 엔지니어링 태스크포스) 가 펴낸 RFC 9110(Request for Comments, 인터넷 기술 표준 문서에 붙이는 번호) 15.5 는 4xx 를 클라이언트 오류 부류로 두고, 이 부류의 상태 코드는 클라이언트가 잘못한 것으로 보인다는 것을 가리킨다고 적습니다. 이것은 부류 전체에 걸리는 규정입니다. 429 하나에 걸리는 것은 아래 명세상 뜻 절이 받습니다.

값 자체는 IANA(Internet Assigned Numbers Authority, 인터넷 주소 할당 기구) 의 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 상태 코드 레지스트리에 올라 있습니다. 429 행의 설명은 Too Many Requests 이고 근거 문서는 RFC 6585 입니다.

명세는 서버가 무엇을 기준으로 세는지까지는 정하지 않습니다. RFC 6585 4절은 이 명세가 오리진 서버가 사용자를 어떻게 식별하는지도, 요청을 어떻게 세는지도 정의하지 않는다고 못 박습니다. 요청 속도를 제한하는 오리진 서버는 자원별 요청 수를 기준으로 셀 수도 있고, 서버 전체를 기준으로 셀 수도 있고, 여러 서버에 걸쳐 셀 수도 있습니다. 사용자를 인증 크리덴셜로 식별할 수도 있고 상태를 가진 쿠키로 식별할 수도 있습니다.

429 를 내는 것 자체도 의무가 아닙니다. RFC 6585 7.2 는 서버가 공격을 받거나 한쪽에서 아주 많은 요청을 받을 때 그 하나하나에 429 로 응답하면 자원을 소모한다고 적습니다. 그래서 서버는 429 상태 코드를 쓰도록 요구되지 않습니다. 자원 사용을 제한할 때는 그냥 연결을 끊거나 다른 조치를 취하는 편이 더 적절할 수 있습니다.

명세상 뜻

429 를 정한 문서는 RFC 6585 Additional HTTP Status Codes 이고, 절은 4절입니다. 2012년 4월에 나온 IETF Standards Track 문서이며 RFC 2616 을 갱신합니다.

정의 문장은 이것입니다. 429 상태 코드는 사용자가 주어진 시간 동안 너무 많은 요청을 보냈음을 가리킵니다. 명세는 그 괄호 안에 "rate limiting" 이라고 적어 둡니다.

함께 정해진 것이 셋입니다. 응답 표현은 그 조건을 설명하는 상세를 담아야 합니다(SHOULD). 응답은 새 요청을 하기 전에 얼마나 기다려야 하는지를 알리는 Retry-After 헤더를 담을 수 있습니다(MAY, 선택 사항입니다). 429 상태 코드가 붙은 응답은 캐시가 저장해서는 안 됩니다(MUST NOT, 하면 안 된다는 뜻입니다).

명세가 든 예시는 이렇습니다.

HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600

<html>
   <head>
      <title>Too Many Requests</title>
   </head>
   <body>
      <h1>Too Many Requests</h1>
      <p>I only allow 50 requests per hour to this Web site per
         logged in user.  Try again soon.</p>
   </body>
</html>

Retry-After 자체를 정의하는 것은 RFC 6585 가 아니라 RFC 9110 10.2.3 입니다.

실제 원인

명세는 사용자가 너무 많이 보냈다고만 말합니다. 현장에서 429 를 보게 되는 자리는 그보다 갈래가 많습니다.

계수 단위가 나 하나가 아닐 때

한도가 무엇을 기준으로 세는지는 서버가 정합니다. 이 문서에서는 그 기준을 계수 단위라고 부릅니다. GitHub REST API(Representational State Transfer Application Programming Interface, 표현적 상태 전이 방식의 응용 프로그램 인터페이스) 문서는 미인증 요청이 요청을 보낸 사용자나 애플리케이션이 아니라 출발지 IP 주소(Internet Protocol, 인터넷 프로토콜 주소)에 연결된다고 적습니다. Docker Hub 는 미인증 사용자가 이미지를 내려받을 때(풀)의 한도를 IPv4 주소 하나 또는 IPv6 /64 서브넷 하나당으로 셉니다. 계수 단위가 IP 이면 내 요청량이 그대로여도 같은 주소를 함께 쓰는 쪽의 요청이 같은 카운트에 함께 잡힙니다.

가리는 법은 인증을 붙였는지입니다. 붙였다면 계수가 IP 가 아니라 크리덴셜 쪽으로 옮겨갑니다.

Docker Hub 사용자 종류 6시간당 풀 한도
Business · Team · Pro (인증) 무제한
Personal (인증) 200
미인증 IPv4 주소 하나 또는 IPv6 /64 서브넷 하나당 100

한도가 여러 벌일 때

GitHub 은 이 한도를 레이트 리밋이라고 부르는데, 1차 레이트 리밋과 2차 레이트 리밋 둘로 나눠 겁니다. 1차 레이트 리밋은 인증 수단마다 다릅니다. 미인증 요청은 시간당 60회입니다. 개인 액세스 토큰은 시간당 5,000회이고, 이 5,000회는 개인 한도 하나로 합산됩니다. GitHub Enterprise Cloud 조직이 소유한 GitHub App 이 대신 보내는 요청은 시간당 15,000회의 더 높은 한도를 가집니다. 같은 문서는 더 높은 한도를 가진 앱의 요청이 더 낮은 한도를 가진 인증 수단의 남은 예산을 줄인다고 적습니다. 15,000회 한도의 앱이 대신 10,000회를 쓰면 개인 액세스 토큰의 5,000회 예산은 소진된 상태가 됩니다. 그 앱에는 아직 5,000회가 남아 있어도 그렇습니다.

GitHub 인증 수단 시간당 1차 한도
미인증 60
개인 액세스 토큰 5,000
Enterprise Cloud 조직이 소유한 앱이 대신 보내는 요청 15,000

2차 레이트 리밋에 걸릴 때

GitHub 은 1차 리밋과 별개로 2차 레이트 리밋을 겁니다. 문서가 드는 조건은 동시 요청 100개 초과, 한 엔드포인트에 대한 분당 900 포인트 초과, GraphQL 엔드포인트의 분당 2,000 포인트 초과, 실시간 60초당 CPU(Central Processing Unit, 중앙처리장치) 시간 90초 초과, 일반적으로 분당 80건 또는 시간당 500건을 넘는 내용 생성 요청, 시간당 2,000건을 넘는 OAuth 액세스 토큰 요청입니다. 포인트가 요청 한 건과 같은 값인지, 엔드포인트마다 다른 가중치가 붙는지는 문서가 밝히지 않습니다. 같은 문서는 이 2차 리밋이 예고 없이 바뀔 수 있다고 적고, 공개되지 않은 이유로 2차 리밋에 걸릴 수도 있다고 적습니다.

시간당 한도가 아직 남아 있는데 429 가 오면 이쪽입니다. 다만 2차 리밋의 상태를 확인할 방법은 없다고 문서가 적습니다.

재시도가 한도를 더 밀어올릴 때

받고 나서 무엇을 했는지가 다음 응답을 바꿉니다. GitHub 문서는 레이트 리밋에 걸린 상태에서 계속 요청을 보내면 그 통합이 차단될 수 있다고 적습니다. 폴링에서 같은 자원이 404 를 되풀이해 돌려주는데도 매 주기마다 계속 요청하면 레이트 리밋을 낭비하고 2차 레이트 리밋을 촉발할 수 있습니다.

같은 429 가 서로 다른 리밋에서 올 때

Docker Hub 에는 풀 레이트 리밋과 남용 방지 레이트 리밋이 따로 있습니다. 둘 다 429 로 옵니다. 문서는 이 둘을 오류 코드로 가릅니다. 남용 방지 리밋은 단순한 429 Too Many Requests 응답을 돌려줍니다. 풀 한도 초과는 링크가 들어간 더 긴 오류 메시지를 돌려줍니다.

풀 한도를 넘긴 상태에서 풀을 요청하면 Docker Hub 가 429 응답 코드와 함께 한도에 도달했으니 인증과 업그레이드로 한도를 올릴 수 있다는 본문을 돌려줍니다. 이 메시지는 Docker CLI(Command Line Interface, 명령줄 인터페이스)나 Docker Engine 로그에 나타납니다.

운영

봐야 할 자리는 응답 헤더입니다. 다만 남은 횟수를 알리는 표준 헤더는 아직 없습니다. RFC 6585 와 RFC 9110 이 정한 것은 Retry-After 뿐이고, 나머지는 벤더가 붙인 헤더이거나 초안 단계입니다.

Retry-After

RFC 9110 10.2.3 이 정의합니다. 서버는 사용자 에이전트가 후속 요청을 하기 전에 얼마나 기다려야 하는지를 알리려고 이 필드를 보냅니다. 값의 형식은 둘입니다.

Retry-After = HTTP-date / delay-seconds
delay-seconds  = 1*DIGIT

delay-seconds 는 초를 나타내는 비음수 십진 정수입니다. Retry-After: Fri, 31 Dec 1999 23:59:59 GMT 와 Retry-After: 120 이 명세가 든 두 예시이고, 뒤쪽은 2분입니다.

창의 길이

한도는 정해진 시간 구간(창) 단위로 셉니다. GitHub 의 1차 한도는 시간당이고, Docker Hub 의 풀 한도는 6시간 단위로 계산됩니다. 창의 길이가 다르면 같은 초당 요청 수에서도 걸리는 시점이 달라집니다.

GitHub 의 x-ratelimit-* 헤더

GitHub 은 응답마다 1차 레이트 리밋의 현재 상태를 헤더로 붙입니다. 철자는 문서에 적힌 그대로입니다.

헤더 값
x-ratelimit-limit 시간당 보낼 수 있는 최대 요청 수
x-ratelimit-remaining 현재 창에 남은 요청 수
x-ratelimit-used 현재 창에서 이미 보낸 요청 수
x-ratelimit-reset 현재 창이 리셋되는 시각. UTC(Coordinated Universal Time, 협정 세계시) [[에포크
x-ratelimit-resource 그 요청이 계수된 레이트 리밋 자원

GET /rate_limit 엔드포인트로도 한도를 확인할 수 있습니다. 이 호출은 1차 레이트 리밋에 계수되지 않지만 2차 레이트 리밋에는 계수될 수 있습니다. 같은 문서는 가능하면 이 엔드포인트를 부르는 대신 응답 헤더를 쓰라고 적습니다. 2차 레이트 리밋의 상태를 확인할 방법은 없습니다.

기다리는 기준도 문서가 값으로 적어 둡니다. retry-after 응답 헤더가 있으면 그 초가 지나기 전에는 재시도하지 않습니다. x-ratelimit-remaining 이 0 이면 x-ratelimit-reset 이 가리키는 UTC epoch 초가 지나기 전에는 재시도하지 않습니다. 둘 다 아니면 최소 1분을 기다립니다. 2차 레이트 리밋 때문에 계속 실패하면 재시도 사이의 대기를 지수적으로 늘리고, 정해 둔 횟수를 넘기면 오류를 냅니다.

flowchart TD
    A{"retry-after 헤더가 있나"} -- 있음 --> B["그 초가 지나기 전에는 재시도 안 함"]
    A -- 없음 --> C{"x-ratelimit-remaining 이 0 인가"}
    C -- 예 --> D["reset 시각이 지나기 전에는 재시도 안 함"]
    C -- 아니오 --> E["최소 1분 대기"]
    B --> F{"2차 레이트 리밋으로 계속 실패하나"}
    D --> F
    E --> F
    F -- 예 --> G["대기를 지수적으로 늘려 재시도. 정해 둔 횟수를 넘기면 오류"]

RateLimit 헤더 필드 초안

IETF httpapi 워킹그룹의 RateLimit header fields for HTTP 가 서버가 쿼터 정책과 현재 서비스 한도를 알리는 헤더 필드를 정의합니다. 이 초안은 레이트 리밋을 쿼터라고 부릅니다. 다만 이것은 RFC 가 아니라 인터넷 드래프트 draft-ietf-httpapi-ratelimit-headers-11 입니다. 문서 자신이 인터넷 드래프트를 참조 자료로 쓰거나 "work in progress" 이외의 방식으로 인용하는 것은 부적절하다고 적습니다. 이 초안은 2026년 11월 24일에 만료될 예정입니다.

초안이 드는 필드 모양 RateLimit: "default";r=50;t=30 은 이렇게 쪼개집니다. 맨 앞의 default 가 정책 이름이고, 나머지가 그 정책의 파라미터입니다.

자리 예시 값 필수 여부 뜻
정책 이름 default — 이 한도가 적용되는 정책의 이름
r 50 필수 해당 정책에서 남은 쿼터
t 30 선택 유효 창. 클라이언트가 그 안에서 남은 쿼터를 넘겨 쓸 수 없는 시간(초)
pk (예시에 없음) 선택 그 요청에 딸린 파티션 키

서버는 파티션 키로 서버 용량을 클라이언트와 자원에 나눠 배정할 수 있고, 쿼터는 파티션 키마다 배정됩니다. 초안은 파티션 전략의 폭이 넓다고 적습니다. 사용자별, 애플리케이션별, HTTP 메서드별, 자원별, 또는 그 조합입니다.

클라이언트는 남은 쿼터가 양수라는 것이 다음 요청도 처리된다는 보장이라고 가정해서는 안 됩니다(MUST NOT). 남은 쿼터가 적으면 서버가 곧 그 클라이언트를 조일 수 있다는 뜻입니다. 클라이언트는 유효 창이 가리키는 시간이 지나면 한도가 전부 복원된다고 가정해서도 안 됩니다(MUST NOT). 서버는 요청과 요청 사이에 남은 쿼터와 유효 창을 임의로 바꿀 수 있습니다(MAY).

Retry-After 와 RateLimit 이 한 응답에 함께 오면 Retry-After 가 우선하고(MUST) 유효 창은 무시될 수 있습니다(MAY). 이때 Retry-After 값은 유효 창의 끝보다 이른 시점을 가리켜서는 안 됩니다(SHOULD NOT).

경계

레이트 리밋에 걸렸는데 403 이 온 경우

이것도 429 인가. 아닙니다. 429 인지 아닌지는 상태 줄(응답 맨 첫 줄에 상태 코드가 실리는 자리. 위 예시의 HTTP/1.1 429 Too Many Requests 줄)에 실린 값 하나로 갈립니다. 리밋에 걸렸다는 사실이 429 를 보장하지는 않습니다. GitHub 문서는 1차 레이트 리밋을 넘기면 403 또는 429 응답을 받게 되고 x-ratelimit-remaining 헤더가 0 이 된다고 적습니다. 2차 레이트 리밋을 넘겼을 때도 403 또는 429 응답과 함께 2차 리밋을 넘겼다는 오류 메시지를 받습니다.

403 은 크리덴셜이 접근을 허용하기에 충분한지를 두고 말하는 값입니다. 그 정의가 무엇까지 정해 두는지는 403 항목이 받습니다. 429 의 정의에는 크리덴셜의 충분함이 들어 있지 않습니다. 429 는 사용자가 주어진 시간 동안 보낸 요청의 수를 두고 말합니다.

관련 항목

값을 정한 문서와 등록처

RFC 6585 · RFC 9110 · IANA · 인터넷 드래프트

헷갈리는 이웃 상태 코드

HTTP 403 · HTTP 404 · HTTP 503

요청을 세는 기준

레이트 리밋 · 쿼터 · 인증 · 크리덴셜 · IP 주소

이것을 실제로 구현·채택한 제품

GitHub · Docker Hub · REST API

다른 이름: 429 · Too Many Requests · 429 Too Many Requests