[개발 깨알 상식_Tips] / HTTP 429와 Retry-After 헤더.md

HTTP 429와 Retry-After 헤더

조회

2026년 6월 23일 | 개발 깨알 상식_Tips


HTTP 429는 단순히 "다시 시도"라고 적고 넘기기엔 꽤 까다로운 실패다. 서버가 바쁜 건지, 내 토큰 한도가 막힌 건지, 같은 요청을 너무 빨리 반복한 건지에 따라 다음 행동이 달라진다. 나는 예전에는 429를 보면 재시도 횟수부터 늘렸는데, 그렇게 해두면 로그에는 실패가 줄어든 것처럼 보여도 실제로는 서버가 알려준 대기 계약을 무시한 채 더 지저분한 부하를 만들 때가 있었다.

HTTP 429와 Retry-After 헤더를 재시도 계약으로 읽는 흐름
429 응답을 받았을 때 상태 코드, Retry-After, 계산한 대기 시간, 멈춤 조건을 한 흐름으로 묶어 읽는 방식.

작은 API 클라이언트에서도 이 차이는 바로 드러난다. 요청이 몇 개 없을 때는 sleep을 대충 넣어도 지나가지만, 배치 작업이나 자동 수집처럼 같은 함수를 여러 번 부르는 곳에서는 429 처리 방식이 전체 작업 시간을 좌우한다. 더 귀찮은 점은 실패 본문에 실제 원인이 들어 있는 경우다. 한도 초과, 인증 범위 부족, 특정 리소스 단위 제한이 모두 429 근처에서 섞여 보이면, 상태 코드만 보고 같은 재시도 정책을 적용하기 어렵다.

상태 코드와 본문을 먼저 나누기

429를 만났을 때 첫 번째 기준은 status, header, body를 따로 저장하는 것이다. 상태 코드는 제어 흐름을 바꾸는 값이고, 헤더는 기다릴 시간을 알려주는 값이고, 본문은 사람이 원인을 읽는 자료다. 이 셋을 한 줄 에러 문자열로 합쳐 버리면 다음 실행에서 무엇을 바꿔야 할지 흐려진다. 특히 서버가 JSON 본문에 `rate_limit_scope`, `reset_at`, `request_id` 같은 힌트를 넣어 주는 API라면 본문을 버리는 재시도 래퍼가 제일 먼저 의심 대상이 된다.

curl -sS -D headers.txt -o body.json -w "http=%{http_code} time=%{time_total}
" API_ENDPOINT

이런 식으로 헤더와 본문을 분리해 두면, 성공/실패 판단과 원인 확인을 따로 할 수 있다. 나는 자동화 스크립트에서 본문 파일은 디버깅 자료, `-w` 요약 줄은 실행 로그, exit code는 다음 단계 진입 조건으로 나눠 보는 편이 덜 헷갈렸다. 한 줄 로그가 깔끔해 보이는 것보다 나중에 실패를 다시 열 수 있는지가 더 중요했다.

Retry-After를 실제 시간으로 바꾸기

`Retry-After` 헤더는 두 가지 모양으로 온다. 하나는 `120`처럼 초 단위 숫자이고, 다른 하나는 HTTP 날짜 형식이다. 숫자면 그대로 대기 시간으로 읽으면 되지만, 날짜면 현재 시각과의 차이를 계산해야 한다. 여기서 흔히 생기는 실수는 날짜 문자열을 그냥 로그에만 남기고 재시도 코드는 고정 sleep을 쓰는 것이다. 그러면 서버가 2분 뒤를 알려 줬는데도 5초마다 두드리는 코드가 된다.

헤더 모양 해석 주의할 점
Retry-After: 30 30초 뒤 재시도 최소 대기 시간으로 보고 jitter를 더할 수 있다
Retry-After: Tue, 23 Jun 2026 12:00:00 GMT 해당 시각 이후 재시도 로컬 timezone이 아니라 HTTP 날짜 기준으로 계산한다
헤더 없음 클라이언트 기본 backoff 사용 바로 반복하지 않고 작은 상한을 둔다

내 기준은 단순하다. 서버가 시간을 줬으면 그 값을 우선하고, 없으면 exponential backoff를 쓰되 너무 오래 잡아먹지 않게 상한을 둔다. 또 여러 worker가 동시에 같은 API를 치는 구조라면 모두가 같은 초에 깨어나지 않도록 짧은 jitter를 붙인다. 여기서 jitter는 성능 트릭이라기보다 재시도 폭주를 피하는 안전장치에 가깝다.

횟수보다 멈춤 조건을 먼저 정하기

429 처리에서 재시도 횟수만 늘리는 방식은 마음이 편하지만 위험하다. 실패가 일시적인 혼잡이면 도움이 되지만, 계정 한도나 잘못된 호출 패턴이면 같은 요청을 더 오래 반복할 뿐이다. 그래서 나는 `최대 5회` 같은 숫자보다 먼저 무엇이 나오면 멈출지를 적어 둔다. 예를 들어 같은 request id가 반복되거나, 본문에 quota exhausted가 보이거나, 계산한 대기 시간이 작업 예산보다 길면 재시도가 아니라 보류 상태로 넘긴다.

이렇게 하면 자동화가 조용히 오래 버티는 문제가 줄어든다. 특히 크론이나 배치에서는 실패가 즉시 보이는 것보다 애매하게 지연되는 쪽이 더 피곤하다. 429를 만나고도 30분 동안 같은 API를 붙잡고 있으면, 뒤의 작업은 실패 원인을 모른 채 밀린다. 재시도 정책은 성공률을 높이는 장치이기도 하지만, 실패를 제때 표면으로 올리는 장치이기도 하다.

로그에는 계산 결과까지 남기기

실패 로그에는 원문 헤더만 남기지 말고 계산한 값도 같이 남기는 편이 좋다. `Retry-After=120`, `sleep=128s`, `attempt=2/5`, `stop_at=600s`처럼 적어 두면 다음에 로그를 볼 때 코드가 서버 지시를 어떻게 해석했는지 바로 보인다. 반대로 `rate limit, retrying`만 남으면 진짜로 120초를 기다렸는지, 5초만 기다렸는지, 혹은 헤더를 못 읽었는지 다시 코드를 열어야 한다.

여기에는 작은 장점이 하나 더 있다. 운영 중 API 제공자가 헤더 정책을 바꾸거나 프록시가 헤더를 누락시키면, 로그의 계산 결과가 먼저 이상해진다. 서버가 아무 값을 안 줬는데도 매번 같은 sleep을 쓰는지, 날짜 파싱이 실패해서 0초가 되는지, worker마다 jitter가 제대로 흩어지는지 확인하기 쉽다. 429는 에러 처리라기보다 외부 시스템과의 속도 조율이므로, 그 조율 과정 자체가 로그에 보여야 한다.

내가 쓰는 작은 기준

요즘 API 호출 래퍼를 만들 때는 429 분기를 이렇게 접어 둔다. 첫째, 본문과 헤더를 버리지 않는다. 둘째, Retry-After가 있으면 그 값을 우선한다. 셋째, 대기 시간이 작업 예산을 넘으면 재시도하지 않고 사람이나 상위 큐가 볼 수 있는 상태로 넘긴다. 넷째, 로그에는 원문과 계산 결과를 함께 남긴다. 이 네 가지를 지키면 재시도 코드는 조금 길어지지만, 실패를 다시 여는 시간은 확실히 줄어든다.

작은 자동화일수록 이런 분기가 과해 보일 때가 있다. 그래도 429는 같은 요청을 더 세게 밀어붙이면 해결되는 오류가 아니다. 서버가 보내는 속도 제한 신호를 읽고, 내가 가진 작업 예산 안에서 어디까지 기다릴지 정하는 문제다. 그래서 나는 이 상태 코드를 볼 때마다 재시도 횟수보다 먼저 헤더, 본문, 멈춤 조건을 같이 확인한다.

한 번 정리해 두면 다른 상태 코드에도 같은 습관이 붙는다. 503은 서비스 가용성 신호로, 401은 인증 갱신 신호로, 409는 충돌 해결 신호로 읽게 된다. 상태 코드를 문자열 하나로 뭉개지 않는 것만으로도 자동화의 다음 행동이 꽤 선명해진다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.