返回博客

故障排查指南

来自 AI API 的 429 Too Many Requests:用退避重试

来自 AI API 的 429 意味着什么以及如何处理:带抖动的指数退避、Retry-After 和 agent 并发,附 Python 和 JavaScript 代码。

作者:CheapTokens Team发布于 更新于 5 分钟阅读

请求循环示意图:一个 429 响应、一个延迟逐渐增长的等待步骤,以及一次重试
本页内容
  1. 429 的含义
  2. 在猜测延迟之前先读取响应头
  3. Python:带抖动的退避
  4. JavaScript:同样的循环
  5. Agent 应如何处理 429
  6. 在 CheapTokens 上运行 agent 循环
  7. 如果仍然失败

来自 AI API 的 429 Too Many Requests 表示请求被拒绝,原因是你发送得太多太快,或者触达了账户上的限制。解决办法是等待并使用指数退避加抖动重试,在响应带有 Retry-After 头时使用它,并限制你的 agent 并行发送的请求数量。不要重试那种实际上是消费或余额限制的 429:等待并不能解决它。

本指南基于 OpenAI 和 Anthropic 的官方速率限制指南,两者均于 2026 年 10 月 22 日核对。两个页面都没有显示最后更新日期。

429 的含义#

OpenAI 将 429 描述为因超出速率限制而被拒绝的请求(rate_limit_error,当流量增长过快时带有代码 slow_down),或者因为达到了硬性消费限制。Anthropic 对 429 也使用 rate_limit_error。消费上限导致的 429 没有 retry-after 头,并且会持续失败,直到访问恢复。

有两个相邻状态容易与它混淆。OpenAI 的 503 server_is_overloaded 和 Anthropic 的 529 overloaded_error 是过载状况,不是你的速率限制。它们通常用同样的重试循环处理,但它们说明的原因不同。

因此有三种情况,每种都需要不同的反应:

响应 原因 该做什么
429 速率限制 每分钟请求数或 token 数过多 等待,用退避重试
429 消费或余额限制 达到账户限制 不要重试;调整限制或充值余额
503 / 529 过载 提供商一侧繁忙 用退避重试,并设置上限

如果你的余额用完了,请阅读“token 不足”错误。对于超时和连接被拒绝等网络层故障,请参阅常见连接错误。

在猜测延迟之前先读取响应头#

OpenAI 在速率受限响应上记录了这些头:Retry-After,以及 x-ratelimit-limit-requests、x-ratelimit-remaining-requests、x-ratelimit-reset-requests 及其 -tokens 变体。其他 API 可能只发送其中一部分,或者一个都不发送,因此编写客户端时应在 Retry-After 存在时使用它,不存在时回退到你自己的时间表。

OpenAI 关于重试的指导,简而言之:

  • 将 Retry-After 视为最小等待时间,并在其上添加一个小的随机延迟。
  • 如果没有它,使用带抖动的指数退避。
  • 同时限制尝试次数和总重试时间。
  • 不要在你自己的重试之上再叠加 SDK 的重试。
  • 不要重试配额或计费错误。
  • 失败的请求仍然计入每分钟限制,因此激进的循环会让情况更糟。
  • 对于流式传输,在你已经消费了某个请求的输出之后,不要重放该请求。

Python:带抖动的退避#

官方 openai 包可用于任何 OpenAI 兼容端点。关闭 SDK 自身的重试(max_retries=0),以免它们与你的重试相乘。模型 id 来自 CheapTokens 文档;不同提供商的 id 不同。

python
import os
import random
import time

from openai import OpenAI, APIStatusError

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
    max_retries=0,  # we retry ourselves
)

RETRYABLE = {429, 500, 502, 503, 504}


def chat_with_retry(model, messages, max_attempts=5, max_total=60.0):
    started = time.monotonic()
    for attempt in range(max_attempts):
        try:
            return client.chat.completions.create(model=model, messages=messages)
        except APIStatusError as err:
            last = attempt == max_attempts - 1
            if err.status_code not in RETRYABLE or last:
                raise
            header = err.response.headers.get("retry-after")
            if header and header.replace(".", "", 1).isdigit():
                delay = float(header) + random.uniform(0, 1)  # minimum wait + jitter
            else:
                delay = random.uniform(0, min(30.0, 2.0 ** attempt))  # full jitter
            if time.monotonic() - started + delay > max_total:
                raise
            time.sleep(delay)

没有头时,延迟的上限按 1、2、4、8 秒增长(上限为 30),而 random.uniform 会在其下选取一个随机值,这样许多 worker 不会都在同一时刻醒来。Retry-After 也可能是 HTTP 日期;上面的代码处理数字形式,否则回退到退避。

JavaScript:同样的循环#

javascript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.cheaptokens.dev/v1",
  apiKey: process.env.CHEAPTOKENS_API_KEY,
  maxRetries: 0, // we retry ourselves
});

const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function chatWithRetry(model, messages, maxAttempts = 5, maxTotalMs = 60000) {
  const started = Date.now();
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await client.chat.completions.create({ model, messages });
    } catch (err) {
      const last = attempt === maxAttempts - 1;
      if (!RETRYABLE.has(err.status) || last) throw err;
      const header = Number(err.headers?.get?.("retry-after"));
      const delayMs = Number.isFinite(header) && header > 0
        ? header * 1000 + Math.random() * 1000 // minimum wait + jitter
        : Math.random() * Math.min(30000, 2 ** attempt * 1000); // full jitter
      if (Date.now() - started + delayMs > maxTotalMs) throw err;
      await sleep(delayMs);
    }
  }
}

当它奏效时,一个速率受限的调用只是耗时更长,然后返回正常的补全。当它在最后一次尝试后仍然失败时,会抛出原始错误,这样你的代码可以记录它,或者把任务排队留待稍后处理。

Agent 应如何处理 429#

Agent 循环会发送许多短调用,并且多个 agent 或工具调用可能同时运行。仅靠重试是不够的;要从源头降低请求速率:

  1. 限制并发。 3–5 个并行调用的信号量是一个合理的起点;只有在错误保持罕见时才提高它。
  2. 共享一个限流器。 如果多个 worker 使用同一个 key,它们共享相同的限制,因此每个 worker 一个重试循环是不够的:为进程使用一个队列或一个信号量。
  3. 全局退避。 当一个调用收到带有延迟的 429 时,暂停整个池那么长时间,而不是只暂停失败的调用。
  4. 让请求更小。 每个请求的 token 更少意味着对每分钟 token 限制的压力更小:裁剪历史和工具输出。关于 token 都去了哪里,请参阅如何估算 AI agent 的成本。
  5. 在不可重试错误上停止。 401、400 或余额错误不会自行修复;快速失败并报告。
python
import asyncio

limit = asyncio.Semaphore(4)  # at most 4 requests in flight


async def run(job):
    async with limit:
        return await call_model(job)  # your async call with the retry loop above

在 CheapTokens 上运行 agent 循环#

对于有许多短调用的 agent 循环,CheapTokens 提供 MiniMax-M2.7:任何时段一个价格,180K 上下文,每次回答最多 8,192 个 token,所有模型共用一个余额。输出价格为每 1M token $0.15,并且总计(输入和输出合计)比 OpenRouter 更便宜。它通过 OpenAI 兼容端点 https://api.cheaptokens.dev/v1 工作。上面的代码已经指向这个端点;从 CheapTokens 文档设置模型 id 并运行它。

500,000 输入、100,000 输出工作负载的实时价格:

模型 费用 OpenRouter
GLM-5.3-Flash $0.014 $0.0282
DeepSeek-V4-Flash $0.0175 $0.0394
MiniMax-M2.7 $0.04 $0.27

总计 500,000 个输入 token 和 100,000 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。

查看所有价格、设置文档,并创建一个 key 来发送你的第一个请求。

如果仍然失败#

  • 第一个请求就 429。 阅读响应体中的错误类型和消息:消费或账户限制的 429 会在重试时持续失败,直到限制被提高或访问恢复,而速率限制的 429 会在暂停后清除。
  • 长循环中 429 持续出现。 你可能在叠加重试:SDK 重试乘以你的重试。将 SDK 设置为零,如示例所示。
  • 错误在批次开始时成串出现。 逐渐提升,而不是一次性启动所有 worker;OpenAI 报告流量增长过快时会出现 slow_down。
  • 流式调用中途失败。 在你消费了输出之后不要盲目重放;决定是重新开始整个回答,还是从已有内容继续。
  • 响应中没有 Retry-After。 代码会回退到带抖动的退避,因此无需更改。