OpenRouter에서 429가 떴다고 해서 계정 한도에 도달했다는 뜻은 아닙니다. 선택한 업스트림 제공업체가 요청을 제한한 경우도 있습니다. 우선 실패한 응답 하나를 온전히 저장하세요. HTTP 상태 코드, 응답 헤더, JSON 본문을 보면 어떤 제한을 해결해야 하는지 판단할 수 있습니다.
설정을 바꾸기 전, 429 응답부터 확인하세요
크레딧을 구매하거나 키를 교체하고 재시도를 붙이기 전에 실패 원인부터 분류해야 합니다. 사람이 읽는 오류 메시지에는 업스트림 제공업체가 전달한 문구가 섞일 수 있습니다. 반면 OpenRouter의 타입 필드와 응답 헤더는 더 신뢰할 만한 근거입니다.
| 확인할 단서 | 가장 가능성 높은 원인 | 다음 조치 |
|---|---|---|
HTTP 429와 함께 X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset이 반환됨 | OpenRouter 플랫폼 제한 | 리셋 시점까지 기다린 뒤 요청 빈도 또는 동시성을 낮춤 |
error.metadata.error_type이 rate_limit_exceeded이고 provider_code 같은 제공업체 세부 정보가 있음 | 업스트림 제공업체 | 대기하거나 다른 제공업체를 허용하거나 모델 폴백을 사용 |
Retry-After가 있음 | 시도된 모든 제공업체가 재시도 힌트를 제공함 | 다음 시도 전 해당 시간만큼 대기 |
| HTTP 402 | 잔액 부족 또는 키별 크레딧 한도 소진 | 크레딧을 충전하거나 키 한도를 변경; 재시도 백오프로는 해결되지 않음 |
HTTP 200 이후 SSE 오류와 finish_reason: "error"가 발생 | 스트리밍 시작 후 실패 | 스트림을 실패로 처리하고 내장된 오류 유형을 확인 |
OpenRouter의 오류 및 디버깅 레퍼런스는 error.code, error.message, 선택 사항인 error.metadata 구조를 정의합니다. 여기에는 error_type = "rate_limit_exceeded"도 포함됩니다. 또한 정상 응답에는 보통 X-RateLimit-* 헤더가 없다고 명시합니다. 제공업체 과부하는 별도로 provider_overloaded로 분류되며, 일반적으로 503에 대응합니다.
OpenRouter 자체 429를 해결하는 방법
OpenRouter 수준의 429는 계정과 모델 등급에 연결된 플랫폼 할당량에 의해 결정됩니다. 공식 속도 제한 문서를 2026년 7월 31일 기준으로 확인한 결과, :free로 끝나는 무료 모델 변형에는 분당 제한과 일일 제한이 모두 적용됩니다.
| 무료 모델 할당량 | 현재 제한 |
|---|---|
| 분당 요청 수 | 20 RPM |
| 누적 크레딧 구매액이 $10 미만일 때의 일일 요청 수 | 50 RPD |
| 누적 크레딧 구매액이 $10 이상일 때의 일일 요청 수 | 1,000 RPD |
제한 정책에 따르면 추가 계정이나 키를 만들어도 전역적으로 관리되는 용량은 늘어나지 않습니다. 따라서 정상 키를 교체해도 플랫폼 속도 제한은 초기화되지 않습니다.
사용량과 크레딧 제한은 GET /api/v1/key 엔드포인트에서 확인할 수 있습니다.
curl https://openrouter.ai/api/v1/key \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
플랫폼 429가 발생했다면 오류 응답의 리셋 헤더를 기준으로, 다음 순서로 조정하세요.
- 즉시 재시도를 멈추고
X-RateLimit-Reset시점까지 기다립니다. - 초당 요청 수만이 아니라 동시 요청 수를 낮춥니다. 병렬 워커가 한꺼번에 몰리면 첫 429를 확인하기도 전에 제한을 넘길 수 있습니다.
- 공유 제한기 뒤에 작업을 큐잉해 모든 워커가 동시에 깨어나 재시도하지 않도록 합니다.
- 작업량이 무료 모델 할당량에 맞지 않는다면, 해당 트래픽을 적절한 유료 모델 변형으로 옮깁니다.
잔액이 음수이거나 키별 크레딧 한도가 소진된 경우에는 402가 나와야 합니다. 반면 크레딧이 있는 계정도 업스트림 제공업체에서 429를 받을 수 있습니다. 비용과 크레딧 문제는 OpenRouter 가격 가이드에서 별도로 다룹니다.
"Provider Returned Error" 429 해결법
제공업체가 반환한 429는 OpenRouter가 업스트림 추론 제공업체까지 요청을 전달했지만, 그 제공업체가 해당 시점에는 요청을 받지 못했다는 의미입니다. 타입 지정 속도 제한 값과 제공업체 메타데이터를 확인하세요. OpenRouter 크레딧을 추가해도 해당 제공업체의 처리 용량이 생기지는 않습니다.
공식 제한 레퍼런스에 따르면 OpenRouter는 오류를 반환하기 전 이미 대체 제공업체를 시도했을 수 있습니다. 또 시도된 모든 제공업체가 재시도 힌트를 제공한 경우 Retry-After를 추가합니다. 실질적인 대응은 다음과 같습니다.
- 같은 요청을 즉시 다시 생성하지 말고
Retry-After를 따릅니다. - 혼잡한 경로 하나만 남게 만드는 지나치게 엄격한 제공업체 제한을 해제합니다.
- 요청에서 제공업체 폴백이 허용되는지 확인합니다.
- 정확히 그 모델을 쓰는 일보다 작업 완료가 중요하다면 모델 폴백을 구성합니다.
크레딧이 있는 Zed 사용자가 moonshotai/kimi-k2:free에서 업스트림 제한을 겪었고, 키를 다시 생성해도 해결되지 않았습니다. Zed 기여자는 다음과 같이 설명했습니다.
“이것은 Zed 오류가 아닙니다. 사용 중인 업스트림 제공업체가 속도 제한을 걸고 있다고 OpenRouter가 알려주는 것입니다.” 출처: zed-industries/zed issue #35153
Retry-After가 짧게 제시됐다면 이를 지키세요. 정확한 모델을 유지하는 것보다 작업 완료가 중요할 때만 무료 모델 폴백을 사용하는 편이 좋습니다.
Janitor AI, Zed, SillyTavern에서 오류가 날 때
원본 오류를 보존하고, 반복 생성은 피하세요. 제공업체 측 실패라면 모델이나 허용된 경로를 변경해야 합니다. 키를 다시 입력하는 것은 클라이언트의 저장 상태를 바로잡을 때만 의미가 있으며, 용량 제한을 초기화하지는 않습니다.
429 루프를 만들지 않는 재시도 방식
재시도는 속도 제한 실패에만 적용하고, 시도 횟수에 상한을 두며, 서버가 요청한 대기 시간을 우선해야 합니다. 재시도 힌트가 없다면 상한이 있는 지수 백오프와 지터를 사용하세요. 그래야 병렬 클라이언트가 동기화되어 또 다른 요청 폭주를 만드는 일을 막을 수 있습니다.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function retryDelayMs(response, attempt) {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const dateMs = Date.parse(retryAfter);
if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
}
const capMs = 30_000;
const exponentialMs = Math.min(capMs, 1000 * 2 ** attempt);
return Math.random() * exponentialMs; // Full jitter
}
async function createChatCompletion(body, maxAttempts = 4) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(
"https://openrouter.ai/api/v1/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
},
);
const raw = await response.text();
let payload;
try {
payload = raw ? JSON.parse(raw) : null;
} catch {
payload = null;
}
if (response.ok) return payload;
const isRateLimit =
response.status === 429 ||
payload?.error?.metadata?.error_type === "rate_limit_exceeded";
if (!isRateLimit || attempt === maxAttempts - 1) {
const error = new Error(payload?.error?.message || raw || `HTTP ${response.status}`);
error.status = response.status;
error.details = payload?.error;
throw error;
}
await sleep(retryDelayMs(response, attempt));
}
}
이 함수는 비스트리밍 응답을 처리합니다. 여러 대기 중인 워커가 동시에 재시작하지 않도록 공유 큐 또는 토큰 버킷 기반의 동시성 제어는 함수 바깥에 구현하세요.
HTTP 200으로 Server-Sent Events가 시작된 뒤에는 상태 코드가 429로 바뀔 수 없습니다. OpenRouter 오류 레퍼런스에 따르면 이후 발생한 실패는 스트림 안에서 오류와 finish_reason: "error"로 전달됩니다. 완료 요청은 실패로 표시하고, 내장된 유형이 rate_limit_exceeded일 때만 재시도하세요. 애플리케이션이 부분 결과를 명시적으로 지원하지 않는 한, 누적된 텍스트를 성공으로 반환해서는 안 됩니다.
FAQ
OpenRouter의 429 provider returned error는 무슨 뜻인가요?
업스트림 추론 제공업체가 자체 속도 또는 용량 제한 때문에 요청을 거부한 것입니다. error.metadata.error_type과 제공업체 메타데이터로 확인하세요.
크레딧이 남아 있는데 왜 OpenRouter 429가 발생하나요?
크레딧이 있는 계정도 제공업체 측 429를 받을 수 있습니다. 잔액 부족이나 키별 크레딧 한도 문제는 보통 402로 나타납니다.
OpenRouter API 키를 새로 만들면 속도 제한이 초기화되나요?
아니요. 추가 키는 전역적으로 관리되는 제한을 늘리지 않습니다. 인증이나 클라이언트 저장 문제를 해결해야 할 때만 키를 교체하세요.
OpenRouter 재시도 전에는 얼마나 기다려야 하나요?
Retry-After가 있으면 이를 사용하세요. 플랫폼 제한이라면 X-RateLimit-Reset을 따릅니다. 둘 다 없다면 상한이 있는 지수 백오프와 지터를 적용하고 최대 시도 횟수는 작게 유지하세요.
OpenRouter가 HTTP 200을 반환했는데도 429로 실패할 수 있나요?
예. 스트리밍이 이미 시작된 경우 그렇습니다. HTTP 상태는 200으로 유지되지만 SSE 스트림이 오류를 보고하고 finish_reason: "error"로 끝납니다. 내장된 오류 유형을 확인해 속도 제한인지 판단하세요.