← Back to Blog

AI API 报 429 Too Many Requests:限流原因与重试实现

Zivv12 min read
429限流排查

429 Too Many Requests 的含义是:请求本身没有问题——Key 有效、地址正确、模型存在,只是当前的调用频率或用量超出了限制,服务端主动把请求拒绝掉。它可能出现在 OpenAI 兼容接口、Anthropic 协议和 Gemini 协议中的任何一个上。这篇文章不针对某个客户端,而是从 API 层把 429 拆开:先判断被限的是哪个维度,再决定是调整调用方式、实现重试,还是更换配额结构。如果你的 429 只出现在 Claude Code 里,可以先看 Claude Code 限速方案,那篇是场景化解法;这篇解决的是所有 API 调用共通的限流问题。

第零步:先拿到原始状态码

很多客户端会把所有失败统一显示成"请求失败"或"服务繁忙",也有工具把上游的 429 包装成自己的提示语,甚至配上误导性的"请检查网络"。排查之前先确认拿到的真的是 429:

  • SDK 调用:捕获异常后打印状态码和完整响应体,不要只打印异常消息
  • 命令行工具:打开 verbose 或 debug 日志,找到原始 HTTP 响应
  • 桌面客户端:在设置里找开发者日志或请求日志

确认状态码是 429 再往下走。如果实际是 401、404 或 529,排查路线完全不同,硬按限流处理只会浪费时间。

429 限的可能是四种不同的东西

多数人看到 429 的第一反应是"每分钟请求太多了",但实际的限流维度至少有四种,对应的处理方式完全不同:

限流维度限的是什么典型表现
RPM(每分钟请求数)单位时间内的请求条数高频小请求触发,单个大请求反而没事
TPM(每分钟 Token 数)单位时间内的 Token 总量长上下文、长输出触发,请求数不多也会中
并发数同时在途的请求数量多线程、批量脚本一起发时触发
账号级配额时间窗口内的总用量等一两分钟没用,要等整个窗口刷新

判断错维度,对策就会失效:RPM 超了需要降低请求频率;TPM 超了要压缩上下文或拆分任务;并发超了要在客户端加并发上限;账号配额用尽则重试再多次也没用,只能等窗口刷新,或者更换供给结构。

第一步:读响应头和错误正文

排查 429 时最有价值的信息不在客户端弹窗里,而在原始响应中。用 curl 直接复现一次:

curl -i https://zivv.pro/v1/chat/completions \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.4","messages":[{"role":"user","content":"ping"}]}'

加上 -i 可以看到完整响应头。重点看三处:

  • Retry-After:服务端建议的等待秒数,有就照做,这是最权威的重试依据
  • x-ratelimit-remaining-requestsx-ratelimit-remaining-tokens 一类的限流头:能直接告诉你剩余额度是按请求数算还是按 Token 算
  • 错误正文的 message 字段:很多实现会写明这次限的是 RPM、TPM 还是并发

三大协议的错误正文特征不同,可以按关键词快速归类:

协议错误特征说明
OpenAI 兼容(/v1rate_limit_exceededmessage 里常写明 RPM 或 TPM
Anthropic 协议"type": "rate_limit_error"状态码 429,注意与 529 过载区分
Gemini 协议(/v1betaRESOURCE_EXHAUSTED限流和配额问题共用这个状态

注意并不是所有网关都会返回全部限流头。如果响应头信息有限,就靠下一步的行为测试来判断。

第二步:区分偶发限流和持续限流

两种情况的处理路线完全不同:

  • 偶发限流:一百次请求里错三五次,等一两秒重试就成功。这是正常现象,任何有配额上限的 API 都会出现,靠重试策略消化即可。
  • 持续限流:连续几十秒内所有请求全部 429。这说明配额已经被整体压住,继续重试只会让恢复更慢。

判断方法很简单:停掉所有调用,等 60 秒,手动发一条最小请求。如果单条请求也被拒,基本可以确定是账号级配额或时间窗口被打满,直接看第五步;如果单条成功,问题出在你的调用模式,看第三步。

还有一种介于两者之间的情况:每天固定时段被限。这通常是共享上游在高峰期的整体紧张,属于错峰能解决的问题——把批处理任务挪到低峰时段执行,比任何重试策略都有效。

第三步:检查自己的调用模式

相当一部分 429 是调用方自己制造的。常见的四种:

  1. 无上限并发:批处理脚本用 asyncio.gather 一次性把几百个请求全部发出去,瞬间打满并发限制
  2. 无退避重试:失败立即原样重发,429 触发更多 429,形成重试风暴,窗口永远恢复不了
  3. 多服务共用一个 Key:线上服务、测试脚本、同事的实验共用配额,互相挤兑,谁也查不清
  4. 流式连接挂着不读:发起流式请求后处理逻辑阻塞,连接长时间占住并发槽位

并发控制的最小实现是一个信号量:

import asyncio

sem = asyncio.Semaphore(5)  # 全局最多 5 个在途请求

async def limited_call(payload):
    async with sem:
        return await call_model(payload)

多服务共用 Key 的问题,正确做法是拆 Key:每个项目、每个自动化任务一个独立 Key,用量互不干扰,出问题也能单独定位。团队场景可以直接用 团队模式 给成员和任务分配独立 Key 与预算。

第四步:实现指数退避加抖动

重试是消化偶发 429 的标准手段,但写法有讲究。可直接使用的实现:

import random
import time

import httpx

MAX_RETRIES = 5

def call_with_backoff(payload):
    for attempt in range(MAX_RETRIES + 1):
        resp = httpx.post(
            "https://zivv.pro/v1/chat/completions",
            headers={"Authorization": "Bearer sk-your-key"},
            json=payload,
            timeout=120,
        )
        if resp.status_code != 429:
            resp.raise_for_status()
            return resp.json()
        if attempt == MAX_RETRIES:
            raise RuntimeError("429 重试次数用尽,请检查配额")
        retry_after = resp.headers.get("retry-after")
        if retry_after:
            delay = float(retry_after)
        else:
            delay = min(2 ** attempt, 30)
        time.sleep(delay + random.uniform(0, 1))

四个要点:

  • 优先使用 Retry-After:服务端明确告诉你等多久,就不要自己猜
  • 指数退避:1、2、4、8 秒递增,设一个 30 秒左右的封顶
  • 加随机抖动:避免大量客户端在同一秒齐步重试,再次互相压垮
  • 只重试该重试的:429 和 5xx 值得重试;400、401、403 重试一百次结果也一样,浪费时间还污染日志

另外注意 SDK 的内建重试:OpenAI、Anthropic 的官方 SDK 默认自带若干次重试,如果你在外面又包了一层退避逻辑,两层会相乘——外层 5 次乘以内层 3 次就是 15 次真实请求。保留一层就够,另一层显式关掉(比如初始化 SDK 时把 max_retries 设为 0)。

第五步:配额天花板不够时怎么办

如果第二步确认是账号级配额持续打满,说明真实工作负载已经超过单账号的供给能力,重试和排队只是延缓失败。此时的选项有三个:

  1. 向官方申请提升限额:适合大客户,但周期长、门槛高
  2. 自建多账号轮询:要自己维护账号池、处理调度和封禁,隐性成本高
  3. 使用带账号池调度的中转:把调度问题交给平台

Zivv 的做法是第三种:你只持有一个 Key,后端自动在账号池内调度,单个上游账号被限时自动切换,对调用方表现为持续可用;计费按实际 Token 用量,不需要为峰值预购配额。三大协议入口一致——OpenAI 兼容用 https://zivv.pro/v1,Anthropic 协议用 https://zivv.pro,Gemini 协议用 https://zivv.pro/v1beta——现有代码只需要换 base_url 和 Key。各错误码的完整含义可查 错误码说明

对 Claude Code 这类重度消耗场景,还可以选择 Claude MAX 分组:基于 Claude 订阅账号池提供原生 Anthropic 协议接入,追求的就是无速率限制体感,具体接入方式见 Claude Code 配置文档

生产系统的限流预算设计

如果 API 调用跑在生产系统里,比排查更重要的是设计。四条实践:

  • 给限额留余量:日常水位控制在限额的 70% 到 80%,峰值才有缓冲空间,而不是贴着上限跑
  • 区分流量优先级:交互式请求(用户在线等结果)和批处理(夜间任务)分开用 Key,批处理主动限速,不和在线流量抢配额
  • 把 429 比例做成监控指标:正常水平应该接近零,持续升高说明负载在逼近天花板,提前处理而不是等雪崩
  • 入口处排队削峰:把突发流量放进队列,用固定速率消费。这样 429 从"随机故障"变成"可计算的延迟",对下游的体验完全不同

常见问题 FAQ

429 的请求会扣费吗? 不会。被限流拒绝的请求没有进入模型推理,不产生 Token 消耗;重试成功的那一次按正常用量计费。如果账单里出现你以为失败了的请求,多半是某次重试成功了而日志只记录了失败。

服务端不返回 Retry-After 怎么办? 按指数退避处理,从 1 到 2 秒起步逐次翻倍。不要用固定半秒的短间隔硬试,那基本等于没有退避。

把客户端超时调大能减少 429 吗? 不能。429 是服务端主动拒绝,秒级返回,和超时无关。如果你的问题是请求跑一半断掉或长时间无响应,看 529 与超时排查

换个模型能绕开限流吗? 有时可以。不同模型或分组的配额通常独立计算,把翻译、摘要这类非关键任务分流到 gemini-3.5-flash 之类的轻量模型,既缓解主力模型的限流,也省成本。

加大并发能在窗口刷新时抢到更多额度吗? 相反。并发越高越快触发限制,部分实现还会对持续超限的调用方延长恢复时间。稳定的低并发加退避,总吞吐反而更高。

结论

429 排查的顺序是:先读响应头确定限流维度,再区分偶发与持续,然后修正自己的并发与重试写法,最后才是配额结构问题。前三步是工程问题,照着做就能解决;最后一步是供给问题,换结构比硬扛有效。如果你已经确认是配额天花板太低,可以注册 Zivv,一个 Key 接入 100+ 模型,账号池自动调度,把限流问题留给平台处理。