Retry-After 헤더: 429, 503, 413, 3xx에서 각각 뜻하는 것과 클라이언트가 읽는 법
RFC 9110과 RFC 6585를 기준으로 Retry-After의 두 가지 값 형식, 상태 코드별 의미, HTTP-date와 초 단위 값을 대기 시간으로 바꾸는 방법, 잘못된 값과 큰 값을 다루는 방법을 정리했습니다.
Retry-After는 서버가 “다음 요청은 이만큼 기다렸다가 보내라”고 알려 주는 응답 헤더입니다. HTTP 의미론 명세 RFC 9110의 10.2.3절에 정의되어 있고, 같은 헤더라도 어떤 상태 코드와 함께 오느냐에 따라 뜻이 조금씩 다릅니다. 값의 형식도 두 가지라서, 클라이언트는 둘 다 읽을 줄 알아야 합니다.
값의 두 가지 형식
Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
Retry-After: 120
문법은 Retry-After = HTTP-date / delay-seconds입니다. 첫 줄은 그 시각까지 기다리라는 뜻이고, 둘째 줄은 응답을 받은 뒤 120초, 곧 2분을 기다리라는 뜻입니다.
delay-seconds는 1*DIGIT, 즉 음이 아닌 10진 정수입니다. 그래서 1.5, -1, +30, 2m은 이 문법에 맞지 않습니다. 기준 시점은 요청을 보낸 때가 아니라 응답을 받은 때입니다.
HTTP-date는 5.6.7절의 날짜 형식입니다. 보내는 쪽은 반드시 IMF-fixdate(Sun, 06 Nov 1994 08:49:37 GMT)로 만들어야 하지만(MUST), 받는 쪽은 예전 형식 두 가지까지 세 형식을 모두 받아들여야 합니다(MUST).
| 형식 | 예 |
|---|---|
| IMF-fixdate (권장) | Sun, 06 Nov 1994 08:49:37 GMT |
| RFC 850 형식 (폐기) | Sunday, 06-Nov-94 08:49:37 GMT |
| asctime 형식 (폐기) | Sun Nov 6 08:49:37 1994 |
세 형식 모두 UTC입니다. asctime 형식에는 시간대 표시가 없지만 UTC로 간주합니다. HTTP-date는 대소문자를 구분하고, 두 자리 연도를 쓰는 RFC 850 형식은 50년 넘게 미래로 보이면 같은 끝 두 자리를 가진 가장 최근의 과거 연도로 해석해야 합니다. 또 헤더 값의 앞뒤 공백은 값에 포함되지 않으므로, 파서는 값을 평가하기 전에 그 공백을 빼야 합니다(5.5절).
상태 코드별 의미
| 상태 코드 | 근거 | Retry-After가 뜻하는 것 | 요구 수준 |
|---|---|---|---|
| 503 Service Unavailable | RFC 9110 10.2.3, 15.6.4절 | 서비스를 쓸 수 없을 것으로 예상되는 기간 | 보내도 됨(MAY) |
| 3xx 리다이렉트 | RFC 9110 10.2.3절 | 리다이렉트된 요청을 보내기 전에 기다려 달라는 최소 시간 | 별도 요구 없음 |
| 413 Content Too Large | RFC 9110 15.5.14절 | 상태가 일시적이라는 것, 그리고 언제부터 다시 시도해도 되는지 | 일시적이면 보내야 함(SHOULD) |
| 429 Too Many Requests | RFC 6585 4절 | 새 요청을 보내기 전에 기다릴 시간 | 보내도 됨(MAY) |
몇 가지는 표만으로는 드러나지 않습니다.
503. 명세는 503을 일시적인 과부하나 예정된 점검 때문에 지금은 요청을 처리할 수 없는 상태로 정의합니다. 다만 과부하 상태의 서버가 꼭 503을 써야 하는 것은 아니고, 연결을 그냥 거부하는 서버도 있다고 적고 있습니다. 503에 Retry-After가 없다고 해서 곧 회복된다는 뜻도 아닙니다.
3xx. 10.2.3절은 모든 3xx 응답에 대해 Retry-After를 리다이렉트된 요청을 보내기 전에 기다려 달라고 요청하는 최소 시간으로 정의합니다. “이 시간 이후에 보내라”가 아니라 “최소한 이만큼은 기다려 달라”는 요청입니다. 다만 모든 3xx에 따라갈 Location이 있는 것은 아닙니다. 15.4절은 301, 302, 303, 307, 308처럼 Location으로 다른 URI를 알려 주는 리다이렉트와, 여러 선택지를 제시하는 300, 저장해 둔 결과를 쓰게 하는 304를 구분합니다. 응답에 Location이 있고 클라이언트가 그 대상으로 리다이렉트된 요청을 보내는 경우라면, 이 최소 대기 시간은 그 요청에 적용됩니다. 리다이렉트 상태 코드 각각의 차이는 301, 302, 307, 308 리다이렉트의 차이에서 다뤘습니다.
413. 요청 본문이 너무 커서 처리하지 않는다는 응답입니다. 명세는 상태가 일시적이라면 서버가 Retry-After를 보내, 일시적이라는 것과 언제부터 다시 시도해도 되는지를 알려야 한다(SHOULD)고 정합니다. 그래서 413에 Retry-After가 붙어 있으면 서버가 일시적인 상태라고 알린 것으로 읽을 수 있습니다. 반대로 Retry-After가 없다고 해서 제한이 영구적이라는 뜻은 아닙니다. SHOULD는 MUST가 아니므로, 일시적인 상황에서도 헤더 없이 413을 보내는 서버가 있을 수 있습니다. 이 헤더 하나만으로는 상태가 일시적인지 판단할 수 없으니, 응답에 담긴 다른 정보나 애플리케이션의 정책을 보고 다시 보낼지를 정해야 합니다.
429. RFC 6585는 429 응답에 상황을 설명하는 내용을 담아야 하고(SHOULD), Retry-After는 넣어도 된다(MAY)고 정합니다. 서버가 사용자를 어떻게 구분하고 요청을 어떻게 세는지는 정의하지 않습니다. 리소스별로 셀 수도, 서버 전체나 여러 서버에 걸쳐 셀 수도 있습니다. 그러니 클라이언트는 기다려야 하는 범위가 그 URL 하나인지, 같은 호스트 전체인지 헤더만으로는 알 수 없습니다. 429 응답은 캐시에 저장하면 안 됩니다(MUST NOT).
10.2.3절의 정의 자체는 특정 상태 코드에 묶여 있지 않습니다. “후속 요청 전에 사용자 에이전트가 기다려야 할 시간”이라는 일반 정의가 있고, 503과 3xx일 때의 의미가 따로 적혀 있는 구조입니다. IANA HTTP 필드 이름 레지스트리에서 Retry-After는 permanent 상태이고 참조 문서는 RFC 9110 10.2.3절입니다.
대기 시간으로 바꾸기
아래는 명세의 규칙을 바탕으로 한 구현 예시입니다. 상한값과 Date 헤더를 쓰는 방식은 명세가 정한 것이 아니라 이 글의 제안입니다. 이 코드는 그대로 실행되지 않는 부분 의사 코드입니다. 명세를 따르는 HTTP-date 파서는 일부러 생략했고, parseHttpDate는 그 자리를 표시하는 이름일 뿐입니다.
// 자리 표시자: IMF-fixdate, RFC 850, asctime 세 형식을 모두 처리하는
// HTTP-date 파서가 들어갈 자리. 이 글에서는 구현하지 않는다.
declare function parseHttpDate(value: string): number | null;
// 기다릴 밀리초를 돌려준다. 해석할 수 없는 값이면 null.
function retryAfterMs(
value: string,
receivedAt: number, // 응답을 받은 시각(ms, 로컬 시계)
serverDate: number | null, // 같은 응답의 Date 헤더(ms). 없으면 null
maxMs: number, // 이 클라이언트가 정한 상한
): number | null {
const v = value.trim();
if (/^[0-9]+$/.test(v)) {
return Math.min(Number(v) * 1000, maxMs);
}
const at = parseHttpDate(v); // 자리 표시자(위 참고)
if (at === null) return null;
const base = serverDate ?? receivedAt;
return Math.min(Math.max(at - base, 0), maxMs);
}
코드에 담은 판단은 이렇습니다.
- 초 단위 값은 응답을 받은 시각부터 셉니다. 명세 문구가 “응답을 받은 뒤”이므로 요청을 보낸 시각을 기준으로 삼으면 왕복 시간만큼 일찍 다시 보내게 됩니다.
- 날짜 값은 서버 시계로 쓴 시각입니다. 클라이언트 시계가 서버와 어긋나 있으면
날짜 - 현재 시각은 그만큼 틀립니다. 그래서 이 예시는 같은 응답에Date헤더가 있으면Retry-After 시각 - Date만큼 기다립니다. 이것은 명세에 적힌 절차가 아니라 이 글의 제안이고, 시계 차이를 피하려는 보수적인 대체 수단일 뿐입니다. RFC 9110 6.6.1절에서 Date는 메시지가 만들어진 시각을 나타내는데, 클라이언트는 응답을 받은 뒤부터 기다리기 시작합니다. 그러니 이 계산은 응답이 만들어진 뒤 클라이언트에 도착하기까지 걸린 시간만큼 더 기다리게 되고, 캐시 같은 중개자가 저장해 둔 응답을 돌려준 경우에는 응답이 저장되어 있던 시간만큼 더 오래 기다릴 수도 있습니다. 또 같은 절에 따르면 시계가 있는 원 서버는 2xx, 3xx, 4xx 응답에는 Date를 반드시 넣어야 하지만 5xx에는 넣지 않아도 됩니다. 503에는 Date가 없을 수 있으므로, 그때 이 예시는 로컬 시계로 잰 수신 시각을 기준으로 삼고 앞에서 말한 시계 차이를 그대로 안게 됩니다. - 이미 지난 시각이면 0으로 봅니다. 명세에 따로 정해진 규칙은 없습니다. 더 기다릴 이유가 없다는 판단입니다.
- 상한을 둡니다.
1*DIGIT문법에는 자릿수 제한이 없어서, 문법상으로는 아주 큰 값도 맞습니다. 정수형으로 바로 바꾸면 넘침이 생길 수 있고, 실수로 들어간 값 때문에 클라이언트가 며칠씩 멈출 수도 있습니다. 상한을 얼마로 할지는 서비스마다 다릅니다. - 해석할 수 없는 값은 없는 것으로 칩니다.
1.5나 형식이 틀린 날짜를 어떻게 처리할지 10.2.3절은 말하지 않습니다. 이 예시는 null을 돌려주고, 호출하는 쪽에서 자체 백오프 규칙을 쓰게 했습니다.
기다린 뒤에도 다시 보내도 되는지는 별개
Retry-After는 언제 다시 보낼지를 알려 줄 뿐, 그 요청을 다시 보내도 안전한지는 말하지 않습니다. RFC 9110 9.2.2절은 멱등하지 않은 메서드의 요청은 실제로 멱등이라는 것을 알거나 원래 요청이 반영되지 않았음을 확인할 방법이 없다면 자동으로 다시 보내지 않아야 한다고 정합니다(SHOULD NOT). 503이나 429에 Retry-After가 붙어 왔다고 해서 이 규칙이 풀리지는 않습니다. 자세한 내용은 HTTP 메서드의 안전과 멱등에 정리했습니다.
10.2.3절의 표현도 “기다려야 한다(ought to)“이고, MUST 같은 요구 수준 키워드를 쓰지 않습니다. 서버가 보내는 것은 권고이고, 이를 따르는 것은 클라이언트의 몫입니다. 3xx일 때의 표현은 조금 더 구체적이어서, 사용자 에이전트에게 기다려 달라고 “요청하는” 최소 시간이라고 적혀 있습니다.
서버에서 보낼 때
아래는 명세 규칙에서 끌어낸 제안입니다.
- 날짜를 쓴다면 IMF-fixdate로 만들어야 합니다(MUST). 그 밖의 형식은 받는 쪽 호환용입니다.
- 초 단위 값은 서버와 클라이언트의 시계 차이에 영향을 받지 않습니다. 점검 종료 시각처럼 정해진 시각이 있을 때는 날짜가 의도를 더 그대로 전합니다.
- 많은 클라이언트에게 같은 날짜를 돌려주면, 그 클라이언트들이 같은 시각에 한꺼번에 돌아올 수 있습니다. 명세가 다루는 내용은 아니지만, 서버가 값을 조금씩 다르게 주거나 클라이언트가 임의의 지연을 더하는 방식을 고려할 만합니다.
- 429에는 왜 막혔는지를 본문에 적어야 합니다(SHOULD). Retry-After만으로는 제한의 범위를 알릴 수 없습니다.
참고한 문서
- RFC 9110 HTTP Semantics: 5.5, 5.6.7, 6.6.1, 9.2.2, 10.2.3, 15.4, 15.5.14, 15.6.4절
- RFC 6585 Additional HTTP Status Codes: 4절
- IANA HTTP Field Name Registry
링크와 내용은 2026년 10월 14일에 확인했습니다.