OpenAI API 429 重试:先判断错误类型,再安全退避
简答:OpenAI API 返回 429 后,不要直接循环重试:先检查响应体中的 error.code、error.type 和 Retry-After。只有确认是临时速率限制或 slow_down,才在 Retry-After 指定的最短等待时间之后,使用带随机抖动的指数退避;余额、配额、支出限制和未知 429 默认停止。下面给出可复制的 Python、Node.js 示例,并修正 Retry-After 较长时不能提前重试的问题。
定义:OpenAI API 429 表示请求未被当前限制条件接受,但它可能是暂时速率限制,也可能是余额、用量或支出上限问题,必须结合响应体判断是否能够重试。
先看三行速查:哪些 429 可以重试
本文是面向开发者的通用 API 接入说明,本站不是 OpenAI 官方站点。以下内容按 OpenAI 错误码文档、OpenAI 速率限制文档和OpenAI 帮助中心 429 排查说明在 2026-11-02 的公开内容整理;兼容网关可能改变错误结构,遇到字段不一致时应以实际服务商文档为准。
| 响应特征 | 是否重试 | 下一步 |
|---|---|---|
429,error.type=rate_limit_error 且明确为临时限流 | 可以有限重试 | 先遵循有效的 Retry-After,没有时使用指数退避和抖动 |
429,error.code=slow_down | 可以有限重试 | 降低请求速率,等待后逐步恢复流量 |
credit_balance_exhausted、organization_usage_limit_exceeded、organization_spend_limit_exceeded、project_spend_limit_exceeded | 不要重试 | 检查余额、组织用量限制或项目支出限制 |
| 错误码缺失、未知,或兼容接口无法确认原因 | 默认停止 | 记录脱敏响应,确认服务商错误定义后再开启显式兜底策略 |
官方资料明确区分了临时速率限制与余额、支出、配额错误。账单、支出或用量限制不会因为重复发送相同请求而恢复,因此不能把所有 HTTP 429 都放入自动重试分支。官方错误码页面还说明,账单类错误的 error.type 可能较宽泛,判断时应优先查看具体的 error.code。
直接复制前先检查:确认请求使用的是预期组织和项目;确认 API Key 没有混用;确认最终 URL、模型和请求方法正确;确认日志不会写入完整 API Key、用户提示词或敏感输出。
Retry-After 的正确处理:它是最低等待时间
如果响应带有有效的 Retry-After,等待时间应至少达到该值,而不是用本地 max_delay 把它截断。假设服务端要求等待 60 秒,而本地最大等待配置是 20 秒,不能等待 20 秒后立即重试;更安全的做法是判断剩余业务截止时间:剩余时间足够就等待至少 60 秒,剩余时间不足就停止、入队或返回明确的超时状态。
OpenAI 速率限制文档将 Retry-After 定义为临时限流错误的最短等待时间,并列出了请求数、Token 数及重置时间等响应头。官方速率限制文档还说明,若有效服务端等待时间超过客户端或 SDK 支持的最大延迟,应延后处理,而不是提前重试。
| 情况 | 处理 |
|---|---|
Retry-After 是有效秒数 | 至少等待该秒数,可在之后增加小幅随机抖动 |
Retry-After 是有效 HTTP-date | 计算目标时间与当前 UTC 时间的差值,过期按 0 处理 |
| 值为负数、无法解析或已过期 | 视为无效,回退到指数退避 |
| 等待时间超过总截止时间 | 停止重试、进入有界队列或返回明确错误 |
没有有效 Retry-After 时,可以使用 min(max_backoff, base × 2^attempt) + random(0, jitter)。这里的最大等待只是无服务端提示时的本地退避上限,不得用来缩短有效的服务端最低等待时间。失败请求仍会计入速率限制,立即重发可能使限流持续更久。
Python 标准库示例:只重试已确认的临时限流
环境:Python 3.9 或更高版本,示例只使用标准库。先设置 API Key 和当前项目可用的模型名称;模型值不要直接照抄示例,应以项目实际可用配置为准。
export OPENAI_API_KEY='替换为你的密钥'
export OPENAI_MODEL='替换为项目可用的模型名称'示例处理秒数和 HTTP-date 两种 Retry-After 格式。对未知 429、账单错误和配额错误直接停止;当服务端等待时间超过本次操作的总截止时间时,也不提前重试。
import json
import os
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
API_KEY = os.environ["OPENAI_API_KEY"]
MODEL = os.environ["OPENAI_MODEL"]
URL = "https://api.openai.com/v1/responses"
NON_RETRYABLE = {
"credit_balance_exhausted",
"organization_usage_limit_exceeded",
"organization_spend_limit_exceeded",
"project_spend_limit_exceeded",
}
def parse_retry_after(value):
if not value:
return None
value = value.strip()
try:
return max(0.0, float(value))
except ValueError:
pass
try:
target = parsedate_to_datetime(value)
if target.tzinfo is None:
target = target.replace(tzinfo=timezone.utc)
return max(0.0, (target - datetime.now(timezone.utc)).total_seconds())
except (TypeError, ValueError, OverflowError):
return None
def read_error(exc):
raw = exc.read().decode("utf-8", errors="replace")
try:
body = json.loads(raw)
except json.JSONDecodeError:
body = {"raw": raw[:2000]}
return exc.code, body, exc.headers
def is_confirmed_temporary_429(status, body):
if status != 429:
return False
error = body.get("error") or {}
code = error.get("code")
error_type = error.get("type")
if code in NON_RETRYABLE:
return False
if code == "slow_down":
return True
return error_type == "rate_limit_error" and code is not None
def request_with_retry(payload, max_attempts=5, total_seconds=45,
base_delay=1.0, max_backoff=20.0, jitter=0.5):
deadline = time.monotonic() + total_seconds
for attempt in range(max_attempts):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("retry deadline reached")
request = Request(
URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=max(1, int(remaining))) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
status, body, headers = read_error(exc)
if not is_confirmed_temporary_429(status, body):
code = (body.get("error") or {}).get("code")
raise RuntimeError(f"stop retry: HTTP {status}, error.code={code!r}") from exc
if attempt == max_attempts - 1:
raise RuntimeError("retry limit reached") from exc
server_wait = parse_retry_after(headers.get("Retry-After"))
fallback = min(max_backoff, base_delay * (2 ** attempt))
fallback += random.uniform(0, jitter)
wait = server_wait if server_wait is not None else fallback
remaining = deadline - time.monotonic()
if wait > remaining:
raise TimeoutError("server wait exceeds retry deadline") from exc
time.sleep(wait)
except URLError as exc:
raise RuntimeError(f"network error: {exc.reason}") from exc
payload = {"model": MODEL, "input": "Reply with the word OK."}
print(request_with_retry(payload))这段代码的关键点是:server_wait 有效时直接作为最低等待值,不再执行 min(max_backoff, server_wait)。如果等待时间太长,代码返回截止时间错误,由上层决定是否入队或提示用户。
Node.js fetch 示例:明确 Node.js 18 或更高版本
本节只覆盖 Node.js 18 或更高版本的内置 fetch 与 AbortController。不再把它描述为旧环境兼容写法;较旧运行环境应升级 Node.js,或由项目自行选择并审核 HTTP 客户端。先检查运行时:
node --version
export OPENAI_API_KEY='替换为你的密钥'
export OPENAI_MODEL='替换为项目可用的模型名称'
node retry-429.mjs示例同样把有效 Retry-After 视为最低等待时间;如果该等待超过总截止时间,则停止,而不是提前发送下一次请求。
const API_KEY = process.env.OPENAI_API_KEY;
const MODEL = process.env.OPENAI_MODEL;
const URL = "https://api.openai.com/v1/responses";
if (!API_KEY || !MODEL) throw new Error("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL");
const NON_RETRYABLE = new Set([
"credit_balance_exhausted",
"organization_usage_limit_exceeded",
"organization_spend_limit_exceeded",
"project_spend_limit_exceeded",
]);
function parseRetryAfter(value) {
if (!value) return null;
const text = value.trim();
const seconds = Number(text);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
const dateMs = Date.parse(text);
if (!Number.isNaN(dateMs)) return Math.max(0, dateMs - Date.now());
return null;
}
function canRetry(status, body) {
if (status !== 429) return false;
const error = body?.error ?? {};
const code = error.code;
if (NON_RETRYABLE.has(code)) return false;
if (code === "slow_down") return true;
return error.type === "rate_limit_error" && code != null;
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function requestWithRetry(payload, options = {}) {
const {
maxAttempts = 5,
maxTotalMs = 45_000,
baseDelayMs = 1_000,
maxBackoffMs = 20_000,
jitterMs = 500,
} = options;
const deadline = Date.now() + maxTotalMs;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error("retry deadline reached");
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), remaining);
let response;
try {
response = await fetch(URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
const text = await response.text();
let body;
try { body = text ? JSON.parse(text) : {}; }
catch { body = { raw: text.slice(0, 2000) }; }
if (response.ok) return body;
if (!canRetry(response.status, body)) {
throw new Error(`stop retry: HTTP ${response.status}, error.code=${body?.error?.code ?? "unknown"}`);
}
if (attempt === maxAttempts - 1) throw new Error("retry limit reached");
const serverWait = parseRetryAfter(response.headers.get("retry-after"));
const fallback = Math.min(maxBackoffMs, baseDelayMs * (2 ** attempt))
+ Math.floor(Math.random() * jitterMs);
const waitMs = serverWait ?? fallback;
if (Date.now() + waitMs > deadline) {
throw new Error("server wait exceeds retry deadline");
}
await sleep(waitMs);
}
}
const result = await requestWithRetry({
model: MODEL,
input: "Reply with the word OK.",
});
console.log(result);如果使用官方 SDK,请先查看已安装版本的重试设置。官方文档说明,SDK 会对符合条件的 429 和 503 自动重试,但对较长 Retry-After 的处理可能受 SDK 版本和配置影响。业务层再增加循环时,要么关闭 SDK 重试,要么把两层的总尝试次数和总截止时间统一计算。
排查顺序与生产环境最小指标
- 凭据:确认 API Key 属于预期组织和项目,检查是否使用了错误环境的密钥。
- 最终 URL:记录脱敏后的主机、路径、请求方法和响应状态;兼容网关要核对是否改写了错误体。
- 模型:确认模型属于当前项目可用范围,不要把一个模型的速率限制套用到另一个模型。
- 限制维度:同时查看请求数、Token 数、并发、短时间流量增长和组织或项目限制。官方帮助中心指出,请求数和 Token 数是独立限制,且限制可能按组织、项目和模型适用。
- 最小请求:使用单请求、低并发和最短提示词复现,避免排查流量与批量任务混在一起。
- 响应体与日志:记录时间、HTTP 状态、脱敏后的
error.type、error.code、Retry-After、请求 ID 和环境名,不记录 API Key 或完整敏感输入。
| 指标 | 观察重点 | 动作 |
|---|---|---|
| 429 比例 | 按模型、项目、路径和时间窗口拆分 | 先区分临时限流与账单、配额错误 |
| Retry-After | 是否存在、是否有效、是否超过截止时间 | 至少等待服务端指定时间;超时则入队或失败 |
| remaining requests/tokens | 哪一种剩余量先接近零 | 分别控制 RPM、TPM 和并发 |
| 连续 slow_down | 流量是否快速爬升 | 降低速率,稳定后再逐步恢复 |
| 账单或配额错误 | 具体错误码和账户限制 | 停止重试,转到账户、组织或项目设置处理 |
流式请求还要区分“流开始前”和“流开始后”:开始前收到 HTTP 错误,可以按错误体分类;已经收到部分输出后,不应简单地自动重放完整请求,因为可能产生重复内容或重复外部副作用。官方速率限制文档明确提醒,流开始后的错误可能以流事件出现。
常见错误:为什么重试仍然没有恢复
最常见的原因不是退避公式错误,而是把不可恢复的 429 当作临时限流。若响应包含余额用尽、组织用量上限、组织支出上限或项目支出上限,应先处理对应设置;若错误码未知,则记录并停止,不要用无限重试掩盖兼容网关的错误映射。
另一个常见原因是 SDK 和业务层重复重试。一次业务请求可能先经过 SDK 的自动重试,再进入外层循环,导致请求次数和等待时间被放大。上线前应明确由哪一层负责重试,并为总尝试次数、总耗时、队列长度和最终失败率设置可观察指标。
常见问题
OpenAI API 429 一定是请求太快吗?
不一定。429 可能是临时速率限制、slow_down,也可能是预付余额用尽、组织用量上限或组织、项目支出上限。应先读取 error.code 和 error.type,而不是只看 HTTP 状态码。
Retry-After 比本地最大等待时间更长怎么办?
不能用本地最大等待时间截断后提前重试。应把有效 Retry-After 当作最低等待时间;如果等待超过总截止时间,就停止、入队或返回明确的超时状态。
没有 Retry-After 时可以马上重试吗?
不建议。应使用带随机抖动的指数退避,并限制最大尝试次数和总耗时。失败请求也会计入速率限制,立即重发可能让问题持续更久。
为什么 RPM 没超过仍会收到 429?
请求数和 Token 数是不同限制维度,短时间集中发送或流量增长过快也可能触发 slow_down。应同时检查 TPM、并发、剩余量、重置时间和流量增长速度。
官方 SDK 已经自动重试,还要自己写循环吗?
通常不要直接叠加。先查看当前已安装 SDK 版本的重试设置;如果业务层需要统一截止时间或队列策略,应关闭 SDK 重试,或把两层的总尝试次数和总时长明确计算。
未知 429 为什么默认停止,而不是继续试几次?
因为未知 429 可能是兼容接口改写后的账单、配额或其他不可恢复错误。无法确认原因时自动重试会放大流量,默认停止并记录更容易控制风险。
流式响应收到部分内容后能否自动重试?
不应简单重放完整请求。流开始前的 HTTP 错误可以按错误体处理;流开始后若已经收到部分输出,应根据请求是否幂等、是否产生外部副作用以及应用状态决定后续动作。