OpenOx

OpenAI API 429 怎么排查?先看错误码,再决定是否重试

更新于 2026-10-28 · openox.cyou

简答:OpenAI API 返回 429 时,先读取响应中的 error.type、error.code、错误消息和 Retry-After,不要直接无限重试。rate_limit_exceeded、slow_down 通常属于临时限流,可降低请求速率并退避;credit_balance_exhausted、organization_usage_limit_exceeded、organization_spend_limit_exceeded 和 project_spend_limit_exceeded 属于余额或用量限制,重试不会恢复访问。本文给出一条可执行的判断路径,以及 Python、Node.js 和 curl 示例。

定义:OpenAI API 的 429 是服务端拒绝当前请求的 HTTP 状态码,具体原因必须结合错误体中的 error.code、响应头和账户限制判断,不能把所有 429 都当成同一种限流。

先试这一步:用错误码把 429 分成两类

本站是第三方 AI API 接入指南站,本文只依据截至 2026-10-28 检索到的 OpenAI 官方文档整理,不代表 OpenAI 官方。最短判断结论是:临时限流可以有限重试,余额、配额和支出上限错误不要继续重试。OpenAI 官方帮助中心说明,429 可能表示速率限制、预付余额用尽、支出上限或组织用量上限;账单相关错误的 error.type 还可能使用较宽泛的 insufficient_quota,因此应优先查看更具体的 error.code。

先保存一次完整但脱敏的响应信息:HTTP 状态、error.type、error.code、错误消息、Retry-After、模型、请求时间、项目或组织标识,以及服务端返回的 request ID。不要把 API Key、Authorization 请求头或完整请求日志发给他人。

响应特征通常表示什么第一步怎么做是否适合重试
rate_limit_exceeded 或请求数、Token 数达到限制短时间内请求过多,或 RPM、TPM 达到限制降低并发和发送频率,检查 Retry-After可以有限重试
slow_down流量增长过快,即使尚未耗尽 RPM 或 TPM 也可能触发减速,逐步恢复流量,遵循 Retry-After可以有限重试
credit_balance_exhausted组织预付余额已用尽检查 API 账单设置和余额不应靠重试解决
organization_usage_limit_exceeded组织达到 OpenAI 分配的用量上限申请提高获批用量上限或联系管理员不应靠重试解决
organization_spend_limit_exceeded组织达到强制执行的支出上限检查组织支出限制,确认是否需要调整或等待重置不应靠重试解决
project_spend_limit_exceeded当前项目达到项目支出上限检查项目设置和实际使用的项目不应靠重试解决

以上代码和处理方向来自 OpenAI 中文帮助中心的 429 排查说明及 OpenAI API 错误码文档。不同接口、SDK 版本或兼容网关可能返回不同错误体;如果没有标准 JSON 错误体,应先保留原始 HTTP 响应,并核对请求是否经过代理或网关。

按这条顺序排查:错误体、响应头、限制页面

  1. 确认确实是 429。不要只看客户端抛出的异常名称。读取 HTTP 状态和原始响应体,确认是否为 JSON;如果返回 HTML、空响应或网关自定义文本,先检查代理层和上游请求是否被改写。
  2. 读取 error.code。先区分临时限流代码和余额、配额、支出限制代码。对于 credit_balance_exhausted、organization_usage_limit_exceeded、organization_spend_limit_exceeded、project_spend_limit_exceeded,不要通过增加重试次数来处理。
  3. 查看 Retry-After。对于临时限流,如果响应包含有效的 Retry-After,应至少等待该秒数再发起请求。官方速率限制文档把它定义为临时速率限制错误的最小等待时间。
  4. 检查请求数和 Token 数两条限制。RPM 是每分钟请求数,TPM 是每分钟 Token 数,两者是独立限制。请求数量没有超限,不代表 Token 数没有超限;提示词过长、输出上限过高、多个任务共享同一 Key,都可能造成 TPM 或组织级限制。
  5. 检查突发流量和并发。即使平均每分钟请求数低于页面显示值,短时间集中发送仍可能触发更短时间窗口的限制。任务队列、循环、批处理、多进程和共享 API Key 都应纳入统计。
  6. 确认组织、项目和模型对应关系。如果账户属于多个组织,确认请求使用的是预期组织和项目;再检查实际模型是否使用了另一个项目的 Key 或默认组织。
  7. 记录失败请求但不要记录密钥。若响应或客户端统计可得,可记录输入和输出 Token;失败的 429 不一定提供完整 usage 字段,不能假设每次响应都有用量统计。

OpenAI 官方文档说明,速率限制可以作用于组织和项目层面,不是简单地按个人用户分配;请求数限制和 Token 数限制也需要分别查看。可通过 OpenAI API 速率限制指南核对响应头和退避行为。

RPM、TPM、并发与 slow_down 怎么区分

概念含义常见触发场景处理方式
RPM每分钟请求数大量小请求、循环或多线程同时调用降低请求频率,增加队列间隔
TPM每分钟 Token 数长上下文、输出上限设置过高、并发请求缩短上下文,合理设置输出 Token 上限,降低并发
并发同一时刻处于处理中的请求数量批处理或多个 worker 同时发送设置并发上限,使用排队和令牌桶
突发流量短时间内流量突然增加定时任务同时启动、批量重试、流量瞬时爬升分批启动,逐步增加流量
slow_down请求速率增长过快即使当前 RPM、TPM 看起来未耗尽,也可能触发遵循 Retry-After,降低速率后逐步恢复

slow_down 不等同于模型暂时过载。当前官方文档将 429、rate_limit_error、slow_down 用于请求速率增长过快的场景;模型过载则可能返回 503、service_unavailable_error 和 server_is_overloaded。因此,处理程序应同时检查 HTTP 状态、error.type 和 error.code,而不是只判断状态码。

如果响应头存在,可关注 x-ratelimit-limit-requests、x-ratelimit-remaining-requests、x-ratelimit-reset-requests,以及对应的 Token 头。项目级 Token 限制也可能出现在响应头中。具体字段是否出现取决于请求和适用限制,未出现时不要自行推断其数值。

可复制的 curl 检查命令

下面命令用于观察状态码、响应头和响应体。命令只从环境变量读取密钥,不打印完整密钥;模型名称必须替换为当前项目实际可用的模型,不能直接照抄未知占位值。

curl --silent --show-error --include \
  --max-time 60 \
  https://api.openai.com/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "替换为当前项目可用模型",
    "input": "仅用于排查 429 的最小请求"
  }'

成功判定是收到 2xx 响应并获得有效 JSON。若收到 429,先看响应头里的 Retry-After,再看 JSON 中的 error.code。如果响应体不是 JSON,先把问题分为“上游 API 返回 429”或“代理层生成 429”,不要将两者混为一谈。

Python:只对白名单中的临时错误重试

示例重点是四点:只对白名单错误重试;优先使用 Retry-After;抖动加入后再次裁剪总等待时间;非 JSON 响应和网络异常都要安全处理。若使用官方 SDK,还要先确认 SDK 是否已经自动重试,避免 SDK 重试与应用层重试叠加。

import json
import os
import random
import time
from datetime import datetime, timezone

import requests

API_KEY = os.environ.get("OPENAI_API_KEY")
MODEL = os.environ.get("OPENAI_MODEL")
URL = "https://api.openai.com/v1/responses"

if not API_KEY or not MODEL:
    raise RuntimeError("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL")

RETRYABLE_CODES = {"rate_limit_exceeded", "slow_down"}
NON_RETRYABLE_CODES = {
    "credit_balance_exhausted",
    "organization_usage_limit_exceeded",
    "organization_spend_limit_exceeded",
    "project_spend_limit_exceeded",
}


def parse_error(response):
    try:
        data = response.json()
        error = data.get("error") or {}
        return error.get("type"), error.get("code"), error.get("message"), data
    except (ValueError, json.JSONDecodeError):
        return None, None, response.text[:500], None


def request_with_retry(max_attempts=4, deadline_seconds=45):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    payload = {
        "model": MODEL,
        "input": "仅用于验证请求链路",
    }
    deadline = time.monotonic() + deadline_seconds

    for attempt in range(max_attempts):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError("已达到总重试时限")

        try:
            response = requests.post(
                URL,
                headers=headers,
                json=payload,
                timeout=min(20, max(1, remaining)),
            )
        except requests.RequestException as exc:
            if attempt == max_attempts - 1:
                raise
            delay = min(2 ** attempt + random.random(), max(0, deadline - time.monotonic()))
            if delay <= 0:
                raise TimeoutError("网络异常且已达到总重试时限") from exc
            time.sleep(delay)
            continue

        if 200 <= response.status_code < 300:
            return response.json()

        error_type, code, message, raw = parse_error(response)
        if response.status_code != 429 or code not in RETRYABLE_CODES:
            raise RuntimeError(
                f"请求停止:HTTP {response.status_code}, "
                f"type={error_type}, code={code}, message={message}"
            )

        if attempt == max_attempts - 1:
            raise RuntimeError(f"重试次数用尽:code={code}, message={message}")

        retry_after = response.headers.get("Retry-After")
        try:
            server_delay = float(retry_after)
        except (TypeError, ValueError):
            server_delay = 0

        fallback_delay = 2 ** attempt
        delay = max(server_delay, fallback_delay) + random.uniform(0, 0.5)
        delay = min(delay, max(0, deadline - time.monotonic()))
        if delay <= 0:
            raise TimeoutError("已达到总重试时限")
        time.sleep(delay)

    raise RuntimeError("请求未完成")


result = request_with_retry()
print("请求成功,已收到 2xx 响应")

这段代码不会对未知 429 自动重试。这样做是有意的:如果网关返回了未识别的错误码,先输出诊断信息,再决定是否修改白名单。若你使用官方 SDK,请关闭 SDK 自动重试或把 SDK 的重试次数、应用层总时限和最大尝试次数统一计算。

Node.js:使用 fetch 实现有限重试

Node.js 示例使用原生 fetch,适合先验证请求链路。它同样只重试明确的临时限流代码,并对空响应或非 JSON 响应做兜底处理。运行前请使用当前 Node.js 版本支持的 fetch,并设置环境变量。

const apiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL;
const url = "https://api.openai.com/v1/responses";

if (!apiKey || !model) {
  throw new Error("请先设置 OPENAI_API_KEY 和 OPENAI_MODEL");
}

const retryableCodes = new Set(["rate_limit_exceeded", "slow_down"]);
const nonRetryableCodes = new Set([
  "credit_balance_exhausted",
  "organization_usage_limit_exceeded",
  "organization_spend_limit_exceeded",
  "project_spend_limit_exceeded"
]);

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function readBody(response) {
  const text = await response.text();
  try {
    return { text, json: text ? JSON.parse(text) : null };
  } catch {
    return { text, json: null };
  }
}

async function callWithRetry(maxAttempts = 4, deadlineMs = 45000) {
  const deadline = Date.now() + deadlineMs;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const remaining = deadline - Date.now();
    if (remaining <= 0) throw new Error("已达到总重试时限");

    let response;
    try {
      response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${apiKey}`
        },
        body: JSON.stringify({
          model,
          input: "仅用于验证请求链路"
        }),
        signal: AbortSignal.timeout(Math.min(20000, remaining))
      });
    } catch (error) {
      if (attempt === maxAttempts - 1) throw error;
      const delay = Math.min(2 ** attempt * 1000 + Math.random() * 500, deadline - Date.now());
      if (delay <= 0) throw new Error("网络异常且已达到总重试时限");
      await sleep(delay);
      continue;
    }

    const body = await readBody(response);
    if (response.status >= 200 && response.status < 300) {
      if (!body.json) throw new Error("成功响应不是有效 JSON");
      return body.json;
    }

    const error = body.json?.error ?? {};
    const code = error.code ?? null;
    if (response.status !== 429 || !retryableCodes.has(code)) {
      throw new Error(`停止请求:HTTP ${response.status}, code=${code}, message=${error.message ?? body.text.slice(0, 500)}`);
    }

    if (attempt === maxAttempts - 1) {
      throw new Error(`重试次数用尽:code=${code}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const serverDelay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 0;
    const fallbackDelay = 2 ** attempt * 1000;
    const delay = Math.min(
      Math.max(serverDelay, fallbackDelay) + Math.random() * 500,
      deadline - Date.now()
    );
    if (delay <= 0) throw new Error("已达到总重试时限");
    await sleep(delay);
  }

  throw new Error("请求未完成");
}

callWithRetry()
  .then(() => console.log("请求成功,已收到 2xx 响应"))
  .catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
  });

如果你改用带自动重试的官方 SDK,不要在外层无条件再套一层相同逻辑。官方文档提醒,SDK 重试和应用层重试叠加后,实际请求次数可能超过预期。

哪些情况不要继续重试:账户与配置检查清单

处理完成后,用一个低频率、低 Token 的最小请求验证。成功判定应是收到 2xx 和有效响应,而不是只看到客户端没有抛出异常。

如何避免重试风暴

  1. 设置最大尝试次数和总重试时限,不要只设置单次请求超时。
  2. 存在有效 Retry-After 时,把它当作最小等待时间;在此基础上加入小幅随机抖动。
  3. 没有 Retry-After 时,使用指数退避,并在加入抖动后再次根据 deadline 裁剪等待时间。
  4. 让同一任务只有一层重试控制:SDK 自动重试、HTTP 客户端重试、队列重试和业务层重试需要统一规划。
  5. 对共享 Key 的所有调用方统计请求数、Token 数和并发,不要只查看单个进程日志。
  6. 失败请求也可能计入每分钟限制,连续重发同一请求可能让限流持续更久。
  7. 流式请求如果已经开始返回内容,后续错误可能以流事件形式出现,不要因为客户端连接中断就自动完整重放请求,尤其要先确认是否已经产生部分输出或副作用。

以上重试原则与失败请求计入限制的说明,均可在 OpenAI API 速率限制指南中核对。对第三方兼容接口,还应以该接口自己的错误码和文档为准。

常见问题

OpenAI API 返回 429 是不是一定限流?

不是。429 可能是临时速率限制,也可能是预付余额耗尽、组织用量上限、组织支出上限或项目支出上限。先读 error.code,再决定是否重试。

429 和 insufficient_quota 有什么区别?

insufficient_quota 是较宽泛的错误类型或标识,账单相关错误仍可能使用它。更具体的原因应优先看 credit_balance_exhausted、organization_usage_limit_exceeded、organization_spend_limit_exceeded 或 project_spend_limit_exceeded。

看到 Retry-After 后还要不要加指数退避?

可以把 Retry-After 作为最低等待时间,再加入小幅随机抖动;如果服务器给出的等待时间已经超过你的总 deadline,就应停止本次重试并延后任务,而不是提前重试。

为什么 RPM 没超,仍然收到 429?

可能是 TPM、并发、短时间突发流量或 slow_down。RPM 和 TPM 是独立限制;slow_down 还可能在流量增长过快、但尚未耗尽显示的 RPM 和 TPM 时出现。

官方 SDK 已经会重试,我还需要自己写重试吗?

不一定。先确认所用 SDK 版本和配置的自动重试行为。如果再加应用层重试,应把 SDK 计入总尝试次数和总等待时间,或者关闭其中一层,避免请求数量成倍增加。

429 响应没有 JSON,应该怎么办?

保留 HTTP 状态、响应头和正文前几百个字符,检查是否由代理、网关或负载均衡层生成。只有确认响应来自上游并包含可识别错误码后,才按 OpenAI 的错误分类处理。

常见问题

OpenAI API 返回 429 是不是一定限流?

不是。429 还可能表示余额耗尽、组织用量上限、组织支出上限或项目支出上限。应先读取 error.code。

429 可以无限重试吗?

不可以。只有明确属于临时限流的错误才适合有限重试;账单、配额和支出上限错误需要先处理账户设置。

Retry-After 不存在时怎么处理?

使用带随机抖动的指数退避,同时限制最大尝试次数和总等待时间;不要连续立即重发。

RPM 没有达到限制,为什么还是 429?

还可能达到 TPM、并发或更短时间窗口限制,也可能触发 slow_down。请求速率增长过快时,即使 RPM 和 TPM 尚未耗尽,也可能收到 429。

相关阅读

参考资料

  1. OpenAI 中文帮助中心:排查 API 速率限制和 429 错误
  2. OpenAI API 官方文档:Error codes
  3. OpenAI API 官方文档:Rate limits