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-requests、x-ratelimit-remaining-tokens一类的限流头:能直接告诉你剩余额度是按请求数算还是按 Token 算- 错误正文的
message字段:很多实现会写明这次限的是 RPM、TPM 还是并发
三大协议的错误正文特征不同,可以按关键词快速归类:
| 协议 | 错误特征 | 说明 |
|---|---|---|
OpenAI 兼容(/v1) | rate_limit_exceeded | message 里常写明 RPM 或 TPM |
| Anthropic 协议 | "type": "rate_limit_error" | 状态码 429,注意与 529 过载区分 |
Gemini 协议(/v1beta) | RESOURCE_EXHAUSTED | 限流和配额问题共用这个状态 |
注意并不是所有网关都会返回全部限流头。如果响应头信息有限,就靠下一步的行为测试来判断。
第二步:区分偶发限流和持续限流
两种情况的处理路线完全不同:
- 偶发限流:一百次请求里错三五次,等一两秒重试就成功。这是正常现象,任何有配额上限的 API 都会出现,靠重试策略消化即可。
- 持续限流:连续几十秒内所有请求全部 429。这说明配额已经被整体压住,继续重试只会让恢复更慢。
判断方法很简单:停掉所有调用,等 60 秒,手动发一条最小请求。如果单条请求也被拒,基本可以确定是账号级配额或时间窗口被打满,直接看第五步;如果单条成功,问题出在你的调用模式,看第三步。
还有一种介于两者之间的情况:每天固定时段被限。这通常是共享上游在高峰期的整体紧张,属于错峰能解决的问题——把批处理任务挪到低峰时段执行,比任何重试策略都有效。
第三步:检查自己的调用模式
相当一部分 429 是调用方自己制造的。常见的四种:
- 无上限并发:批处理脚本用
asyncio.gather一次性把几百个请求全部发出去,瞬间打满并发限制 - 无退避重试:失败立即原样重发,429 触发更多 429,形成重试风暴,窗口永远恢复不了
- 多服务共用一个 Key:线上服务、测试脚本、同事的实验共用配额,互相挤兑,谁也查不清
- 流式连接挂着不读:发起流式请求后处理逻辑阻塞,连接长时间占住并发槽位
并发控制的最小实现是一个信号量:
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)。
第五步:配额天花板不够时怎么办
如果第二步确认是账号级配额持续打满,说明真实工作负载已经超过单账号的供给能力,重试和排队只是延缓失败。此时的选项有三个:
- 向官方申请提升限额:适合大客户,但周期长、门槛高
- 自建多账号轮询:要自己维护账号池、处理调度和封禁,隐性成本高
- 使用带账号池调度的中转:把调度问题交给平台
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+ 模型,账号池自动调度,把限流问题留给平台处理。