Crazyrouter

OpenAI API 429 重试:先判断错误类型,再安全退避

更新于 2026-11-02 · crazyrouter.cyou

简答: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 的公开内容整理;兼容网关可能改变错误结构,遇到字段不一致时应以实际服务商文档为准。

OpenAI API 429 重试决策表
响应特征是否重试下一步
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 重试,要么把两层的总尝试次数和总截止时间统一计算。

排查顺序与生产环境最小指标

  1. 凭据:确认 API Key 属于预期组织和项目,检查是否使用了错误环境的密钥。
  2. 最终 URL:记录脱敏后的主机、路径、请求方法和响应状态;兼容网关要核对是否改写了错误体。
  3. 模型:确认模型属于当前项目可用范围,不要把一个模型的速率限制套用到另一个模型。
  4. 限制维度:同时查看请求数、Token 数、并发、短时间流量增长和组织或项目限制。官方帮助中心指出,请求数和 Token 数是独立限制,且限制可能按组织、项目和模型适用。
  5. 最小请求:使用单请求、低并发和最短提示词复现,避免排查流量与批量任务混在一起。
  6. 响应体与日志:记录时间、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 错误可以按错误体处理;流开始后若已经收到部分输出,应根据请求是否幂等、是否产生外部副作用以及应用状态决定后续动作。

相关阅读

参考资料

  1. OpenAI API 错误码文档
  2. OpenAI API 速率限制文档
  3. OpenAI 帮助中心:排查 API 速率限制和 429 错误