짧은 시간에 요청이 몰리지 않도록 파트너별로 초당 호출 수를 제한합니다.
기본 한도
| 항목 | 값 |
|---|---|
| 단위 | 파트너별 (같은 파트너의 모든 클라이언트·API 호출을 합산) |
| 한도 | 초당 20회 |
| 순간 허용량 | 최대 20회까지 한 번에 보낼 수 있고, 이후 초당 20회씩 다시 채워집니다 |
한도는 서비스 상황에 따라 조정될 수 있습니다. 더 높은 한도가 필요하면 모비두 운영팀에 문의해 주세요.
한도를 넘으면
HTTP 429 Too Many Requests 를 돌려주고, 아래 헤더로 언제 다시 호출하면 되는지 알려 줍니다.
| 헤더 | 예시 | 뜻 |
|---|---|---|
Retry-After | 1 | 다시 호출하기까지 기다릴 시간(초). 이 값을 기준으로 재시도하세요 |
X-RateLimit-Limit | 20 | 적용된 한도(초당 호출 수) |
X-RateLimit-Remaining | 0 | 남은 호출 수. 429 응답에서는 항상 0 |
X-RateLimit-Reset | 1761634801 | 다시 호출할 수 있는 시각(Unix 초) |
X-Request-Id | 3f9c2a7e-5b1d-4c8a-9e6f-2d7b8a1c4e90 | 요청 추적 ID. 모든 응답에 붙습니다 |
정상 응답(200)에는 X-RateLimit-* 헤더가 붙지 않습니다.
권장 처리 방법
- 429 를 받으면
Retry-After초만큼 기다린 뒤 다시 보냅니다. 바로 다시 보내면 계속 429 가 납니다. - 연속으로 429 가 나면 대기 시간을 늘립니다. 1초 → 2초 → 4초처럼 두 배씩 늘리고, 여러 서버가 동시에 재시도하지 않도록 0~1초 사이의 임의 시간을 더하세요. 5번 정도 실패하면 멈추고 원인을 확인하세요.
- 대량 조회는 미리 속도를 맞춥니다. 여러 방송·기간을 한꺼번에 조회할 때는 초당 20회를 넘지 않도록 요청 사이에 간격(예: 50ms 이상)을 두거나 동시 요청 수를 제한하세요. 같은 파트너의 여러 서버가 호출한다면 한도를 함께 나눠 씁니다.
- 바뀌지 않는 결과는 저장해 두고 재사용합니다. 지난 기간의 통계는 다시 바뀌지 않습니다. 같은 조건을 반복 조회하지 마세요.
재시도 예시
async function callWithRetry(url, options, maxRetry = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 || attempt >= maxRetry) return res;
const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
const backoff = Math.max(retryAfter, 2 ** attempt); // 1, 2, 4, 8 ...
const jitter = Math.random(); // 0~1초
await new Promise(r => setTimeout(r, (backoff + jitter) * 1000));
}
}HttpResponse<String> sendWithRetry(HttpClient client, HttpRequest req, int maxRetry)
throws IOException, InterruptedException {
for (int attempt = 0; ; attempt++) {
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 429 || attempt >= maxRetry) return res;
long retryAfter = res.headers().firstValueAsLong("Retry-After").orElse(1);
long backoff = Math.max(retryAfter, 1L << attempt); // 1, 2, 4, 8 ...
long jitterMs = ThreadLocalRandom.current().nextLong(1000); // 0~1초
Thread.sleep(backoff * 1000 + jitterMs);
}
}fun sendWithRetry(client: HttpClient, req: HttpRequest, maxRetry: Int = 5): HttpResponse<String> {
var attempt = 0
while (true) {
val res = client.send(req, HttpResponse.BodyHandlers.ofString())
if (res.statusCode() != 429 || attempt >= maxRetry) return res
val retryAfter = res.headers().firstValueAsLong("Retry-After").orElse(1)
val backoff = maxOf(retryAfter, 1L shl attempt) // 1, 2, 4, 8 ...
Thread.sleep(backoff * 1000 + Random.nextLong(1000)) // + 0~1초
attempt++
}
}import random
import time
import requests
def get_with_retry(url, headers, max_retry=5):
for attempt in range(max_retry + 1):
res = requests.get(url, headers=headers)
if res.status_code != 429 or attempt == max_retry:
return res
retry_after = int(res.headers.get('Retry-After', 1))
backoff = max(retry_after, 2 ** attempt) # 1, 2, 4, 8 ...
time.sleep(backoff + random.random()) # + 0~1초