如何区分 OpenAI API 配额耗尽与普通限流的 429 错误?
结论
不能只看 HTTP 状态码 429 来决定是否重试。OpenAI API 的 429 至少有两种情况:
insufficient_quota:账户、项目或组织已无可用额度。重试通常无法解决问题,应立即终止 retry loop。- 普通速率限制:请求在短时间内超过 RPM、TPM 等限制,可以使用 exponential backoff 重试。
程序应先检查错误响应中的结构化字段 error.code,再视情况检查 error.type。如果旧版 SDK、代理服务或异常响应没有提供这些字段,最后再通过错误消息字符串进行兼容判断。
两类 429 的区别
额度不足时,响应可能类似:
{
"error": {
"message": "You exceeded your current quota, please check your plan and billing details.",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_quota"
}
}
这表示当前凭据对应的账户、项目或组织已经无法继续产生 API 消耗。等待几秒后再次请求通常没有意义。
普通限流也会返回 429,但它表示调用速度超过了当前模型或项目的速率限制。这类错误可以等待一段时间后重试,响应中还可能包含 Retry-After 或 x-ratelimit-* 相关响应头。
API 的具体错误字段可能随接口、SDK 版本或中间代理而变化,建议按以下顺序检查:
error.codeerror.type- HTTP 状态码和响应头
error.message
Python SDK 中的处理方式
按照当前 Python SDK 的常见用法,所有 429 通常都会表现为 openai.RateLimitError,但异常对象一般还包含解析后的 code、type 或 body。
import openai
client = openai.OpenAI()
try:
response = client.responses.create(
model="gpt-5",
input="Hello"
)
except openai.RateLimitError as exc:
error_code = getattr(exc, "code", None)
error_type = getattr(exc, "type", None)
body = getattr(exc, "body", None)
if isinstance(body, dict):
error = body.get("error", body)
if isinstance(error, dict):
error_code = error_code or error.get("code")
error_type = error_type or error.get("type")
if error_code == "insufficient_quota" or error_type == "insufficient_quota":
raise RuntimeError(
"OpenAI API quota is unavailable; retrying will not resolve it."
) from exc
# 其他 429 可以交给上层重试机制处理
raise
不同版本的 SDK 可能以不同方式暴露异常响应。调试时可以记录 exc.code、exc.type 和 exc.body,但不要记录 API key、完整请求内容或其他敏感信息。
配合 Tenacity 只重试可恢复错误
使用 Tenacity 时,不应重试所有 RateLimitError。需要提供一个判断函数,明确排除 insufficient_quota。
import logging
import openai
from tenacity import (
retry,
retry_if_exception,
stop_after_attempt,
wait_random_exponential,
)
logger = logging.getLogger(__name__)
client = openai.OpenAI()
def get_openai_error_fields(exc):
code = getattr(exc, "code", None)
error_type = getattr(exc, "type", None)
message = str(exc)
body = getattr(exc, "body", None)
if isinstance(body, dict):
error = body.get("error", body)
if isinstance(error, dict):
code = code or error.get("code")
error_type = error_type or error.get("type")
message = error.get("message") or message
return code, error_type, message
def is_retryable_openai_error(exc):
if isinstance(exc, openai.RateLimitError):
code, error_type, message = get_openai_error_fields(exc)
if code == "insufficient_quota" or error_type == "insufficient_quota":
return False
# 仅作为缺少结构化字段时的兼容措施
normalized_message = message.lower()
if (
code is None
and error_type is None
and "exceeded your current quota" in normalized_message
):
return False
return True
# 服务器端暂时性错误通常可以重试
if isinstance(exc, openai.APIStatusError):
return exc.status_code >= 500
# 网络连接错误通常可以重试
return isinstance(exc, openai.APIConnectionError)
@retry(
retry=retry_if_exception(is_retryable_openai_error),
wait=wait_random_exponential(min=1, max=30),
stop=stop_after_attempt(6),
reraise=True,
)
def call_openai():
return client.responses.create(
model="gpt-5",
input="Hello"
)
try:
result = call_openai()
except openai.RateLimitError as exc:
code, error_type, _ = get_openai_error_fields(exc)
if code == "insufficient_quota" or error_type == "insufficient_quota":
logger.error("OpenAI API quota unavailable; request was not retried.")
else:
logger.error("OpenAI rate limit remained active after retries.")
raise
这里的 reraise=True 很重要。当重试条件返回 False 时,原始异常会立即向上传递,不会被包装成难以判断的重试异常。
即使错误可以重试,也必须设置 stop_after_attempt 或 stop_after_delay。无限重试会增加延迟,失败请求还可能计入某些限制窗口。
额度不足时应检查什么
Dashboard 显示”没有超过 hard limit”,并不能排除 insufficient_quota。还需要检查:
- API 计费是否已启用,付款方式是否有效。
- 预付余额或 API credits 是否已经耗尽或过期。
- API key 是否属于预期的 project 或 organization。
- 当前 project 是否设置了单独的预算或使用限制。
- 免费试用额度是否已过期。
- 修改计费设置后,旧 key 或项目配置是否仍然指向原来的账户范围。
ChatGPT 订阅和 API 计费通常是分开的。即使拥有 ChatGPT 付费套餐,也不代表 API 账户自动具有可用余额。
RPM 较低引发的是普通速率限制,与 insufficient_quota 不是一回事。如果响应明确给出 code: "insufficient_quota",应重点检查 API billing、credits、project 和 organization,而不是只降低请求频率。
不建议只匹配错误消息
下面的处理方式很脆弱:
if "quota" in str(exc).lower():
...
错误文案可能发生变化,也可能被本地化或由 SDK 包装。普通限流的消息中同样可能出现 quota。更可靠的检查顺序是:
error.code → error.type → status/headers → message fallback
如果响应确实没有结构化字段,可以通过完整且稳定的短语进行兼容匹配,同时记录脱敏后的响应体,方便后续确认。消息匹配只应作为降级方案,不能作为主要判断依据。
备注:内容仅供参考。
