MCP 호출에서 HTTP 429가 반복된다면, 재시도 횟수부터 늘리지 말고 응답을 만든 구간·제한 범위·다음 요청 가능 시점을 확인한다. 같은 계정을 쓰는 여러 작업자가 따로 기다렸다가 한꺼번에 재개하면 제한은 다시 걸릴 수 있다. 기다림을 개별 호출의 문제가 아니라 공유된 요청량의 문제로 봐야 하는 경우다.
이 글은 MCP 2026-07-28 Streamable HTTP 연결의 HTTP 429 진단을 다룬다. HTTP 기준은 RFC 6585와 RFC 9110이며 확인일은 2026년 10월 14일이다. 아래 시간표와 운영 규칙은 가상 사례와 설계 제안이다. 실제 서비스에 부하를 주거나 장애를 재현한 결과는 아니다.
429와 도구 내부 오류를 먼저 나눈다
RFC 6585 §4의 429는 일정 시간 동안 요청이 너무 많았다는 뜻이다. 제한을 계정·리소스·서버 중 무엇을 기준으로 세는지는 이 규격이 정하지 않는다. 따라서 메시지에 429라는 숫자가 보였다는 이유만으로 분당 허용량이나 제한 대상을 추정해서는 안 된다.
| 관측한 응답 | 먼저 확인할 곳 |
|---|---|
| HTTP 상태 자체가 429 | 게이트웨이·MCP 서버 등 실제 응답 생성 구간과 해당 서비스의 제한 정책 |
| HTTP 200 안의 도구 오류가 외부 API 429를 설명 | 도구가 호출한 하위 서비스와 서버 측 재시도 정책 |
| 응답 없이 시간초과 | 429로 분류하지 말고 연결과 실제 작업 상태 |
| 401·403 또는 명시적 권한 거절 | 요청량 조절보다 지원되는 인증·권한 확인 |
MCP Streamable HTTP에서는 클라이언트가 보내는 JSON-RPC 메시지가 HTTP POST로 전달된다. 바깥의 HTTP 상태와 안쪽 도구 결과는 서로 다른 층이다. HTTP 200으로 도착한 도구 오류의 설명을 HTTP 429 응답과 동일하게 파싱하면, 읽어야 할 헤더도 잘못 고르게 된다.
Retry-After는 초 또는 날짜다
RFC 9110 §10.2.3에서 Retry-After는 응답 수신 뒤 기다릴 초 수 또는 HTTP 날짜다. 초 수는 음수가 아닌 십진 정수다. 429 응답에 이 헤더가 포함될 수 있지만 항상 있어야 하는 것은 아니다. 없다는 사실을 즉시 재시도하라는 뜻으로 읽지 않는다.
Retry-After: 120
# Alternative form, not a second header in the same response:
Retry-After: Wed, 14 Oct 2026 00:02:00 GMT
첫 예시는 응답을 받은 뒤 120초를 기다리라는 뜻이다. 두 번째 예시는 UTC의 특정 시각이다. 가상으로 UTC 00:00:00에 수신했다면 두 값 모두 00:02:00을 가리킨다. 한국 시간으로는 09:02:00이다. 실제 날짜형 값을 처리할 때는 클라이언트 시계의 오차를 점검한다. 과거 날짜나 파싱 실패를 무제한 즉시 재시도로 바꾸지 않는다.
대기 시간이 길어 작업의 전체 제한 시간을 넘는다면, 값을 임의로 줄여 다시 보내지 않는다. 작업을 대기 상태로 돌리거나 나중에 재개할 조건을 남기는 편이 낫다. Retry-After: 0도 재시도 승인이나 성공 보장이 아니다. 요청의 안전성과 서비스 계약은 별도로 확인한다.
각자 기다리면 다시 몰리는 가상 사례
작업자 셋이 하나의 계정 한도를 공유한다고 가정하자. 각 작업자가 429를 받은 뒤 같은 시각에 다시 요청한다. 개별 작업자는 대기 규칙을 지켰지만 합산 요청량은 여전히 클 수 있다. 이때 작업자 수만 늘리면 대기열과 재시도가 함께 늘어난다.
worker A: 429 -> wait -> retry
worker B: 429 -> wait -> retry
worker C: 429 -> wait -> retry
shared scope: one account
question: who controls the combined request rate?
이 가상 상황에 대한 설계 제안은 제한 범위별 대기열과 동시 실행 상한을 두는 것이다. 서버가 알려 준 대기 시점보다 일찍 보내지 않으면서 재개 시점을 분산하고, 반복 실패에는 서비스 계약에 맞춘 제한된 백오프를 적용한다. 분산 폭·최대 횟수·전체 대기 예산은 운영 조건으로 정해야 한다. 이 글은 모든 서비스에 통하는 숫자를 제시하지 않는다.
앱, SDK, MCP 서버가 각각 자동 재시도를 하면 한 번의 사용자 동작이 여러 요청으로 늘 수 있다. 실제 재시도 담당을 한 곳으로 정하거나 각 계층의 시도 횟수를 합산해 제한한다. SDK 설정을 바꾸기 전에는 설치된 버전과 기본 재시도 정책을 문서에서 확인한다. 여기서는 특정 SDK의 기본값을 가정하지 않는다.
제한을 건 응답과 실제 실행 결과를 대조한다
429를 누가 만들었는지 모르면 쓰기 작업이 미실행됐다고 단정할 수 없다. 특히 도구가 하위 서비스를 호출한 뒤 오류를 반환했다면 앞 단계의 변경은 이미 남아 있을 수 있다. 허용된 상태 조회와 서비스의 멱등성 계약으로 결과를 먼저 확인한다. 대기 시간이 지났다는 사실만으로 같은 쓰기 요청을 다시 보내지는 않는다.
계정·토큰·IP를 바꾸어 한도를 피하거나, 거절된 작업을 다른 도구로 돌려서 처리하지 않는다. 401·403과 명시적 권한 거절은 이 글의 자동 재시도 대상이 아니다. 같은 계정에서 작업량을 줄이고 지원되는 사용량·상태 화면으로 원인을 확인하는 범위에서 진단한다.
복사해서 채우는 관측 기록
observed_at_utc: <response received time>
http_status: <actual status>
error_layer: <gateway/mcp/downstream/unknown>
retry_after_kind: <seconds/date/absent/invalid>
retry_after_value: <sanitized value>
limit_scope: <documented scope/unknown>
retry_owner: <app/sdk/server/unknown>
combined_attempts: <observed/unknown>
side_effect_state: <confirmed/unknown/not-applicable>
next_action: <wait/inspect-state/stop>
이 양식은 MCP 필드가 아니라 진단용 메모다. 모르는 값은 unknown으로 남긴다. 계정 식별자·인증 헤더·쿠키·요청 본문을 넣을 필요는 없다. 제공된 요청 ID로 내부 로그를 대조할 수는 있지만, 외부 공유본에는 식별 가능성과 민감한 값을 검토해 제거한다.
| 확인 결과 | 다음 행동 |
|---|---|
| 문서에 제한 범위가 명시됨 | 그 범위를 공유하는 작업의 합산 요청량 확인 |
| 유효한 Retry-After 있음 | 초·날짜를 구분하고 대기 시점과 전체 예산 기록 |
| 헤더가 없거나 해석 불가 | 공식 오류 설명과 클라이언트 정책 확인, 무한 재시도 금지 |
| 여러 계층에서 재시도 | 전체 시도 수를 관측하고 재시도 책임 정리 |
| 쓰기 결과 미확정 | 허용된 결과 조회 또는 멱등성 계약 확인 후 판단 |
조정 뒤에는 승인된 읽기 요청의 작은 범위에서 응답 생성 구간, 429 빈도, 실제 시도 수와 완료 여부를 다시 확인한다. 429가 사라져도 요청이 조용히 버려졌다면 해결이 아니다. 접수한 작업과 완료한 작업이 연결되는지까지 확인해야 한다.
쓰기 응답이 불확실한 경우의 판단은 재시도 전 결과 확인에서 더 다룬다. 429 없이 응답 내용이 몰려 나오는 문제라면 SSE 응답 버퍼링 진단으로 구분한다. 이 글의 핵심 관측 대상은 지연 시간 자체보다 제한 범위와 합산 요청량이다.
적용 한계: RFC의 HTTP 의미와 MCP 2026-07-28 전송 경계를 설명했다. 서비스별 할당량, 계정 요금제, SDK 재시도 구현은 검증하지 않았다. 현장 기록에는 사용 중인 클라이언트·SDK 버전과 제한 정책 확인일을 별도로 남겨야 한다.