HTTP 429 是 LLM API 里信息量最低的报错:三种本质完全不同的故障共用这一个状态码,而三种故障的修法各不相同。重试代码只能修其中一种;对第二种,重试只是推迟必然的失败;对第三种,重试纯属浪费。这篇是错误码速查页背后的 429 深挖:怎么分辨你撞上的是哪一种,唯一值得重试的那种该怎么写生产级重试,以及让 429 从源头减少的结构性改法。
一个状态码,三种故障
| 实际发生了什么 | 限制在哪一层 | 修法 |
|---|---|---|
| 上游供应商限流(RPM/TPM 超限) | 模型厂商的基础设施 | 有上限的指数退避 + 抖动;有 Retry-After 就照做 |
| 网关对 Key 或账户的限额 | Key 的 rateLimit / tokenLimitTpm,或账户共享的限额,由 X-RateLimit-Scope 指明 | 分散或隔离负载,或申请调高限额——退避只能让你在限额以下排队 |
OpenAI 的 insufficient_quota | 官方 OpenAI API 的账单账户 | 解决账单问题;重试毫无意义 |
误判方向的代价是双向的。把网关限额当作会自行消散的上游问题,你会写出一套对着永远不动的天花板反复冲撞的重试逻辑;把 insufficient_quota 当瞬时限流,你会对着一个没有余额的账户白白重试几个小时。
成因一:上游供应商在限流
模型厂商都有每分钟请求数和每分钟 token 数的上限,负载高峰时还会主动用 429(以及 Anthropic 风格的 529 过载响应)甩流量——哪怕你自己的请求频率没变。这是唯一一种客户端重试算对症下药的情况,但必须是有边界的重试,三个要素缺一不可:
- 带上限的指数退避。 每次失败等待时间翻倍,但要有封顶——不封顶的话,第五次重试可能为一个几秒就恢复的限流干等几分钟。
- 抖动(jitter)。 每次延迟加随机量。没有抖动,同一批失败的客户端会在同一时刻集体重试,同步的重试波会再次触发限流。
- 放弃路径。 固定次数之后把错误抛出去。无限循环不是韧性,是一队越攒越陈旧的任务。
响应里带 Retry-After 头时照做——服务端已经明确告诉你容量什么时候恢复,这时还用退避去猜只会拖长故障时间。下面是 Python 版本,用标准 OpenAI 客户端指向 Router One:
import os
import random
import time
from openai import OpenAI, RateLimitError
client = OpenAI(
base_url="https://api.router.one/v1",
api_key=os.environ["ROUTER_ONE_API_KEY"],
)
MAX_ATTEMPTS = 5
BASE_DELAY = 1.0 # 秒
MAX_DELAY = 30.0 # 封顶:最长等这么久
def chat_with_backoff(**kwargs):
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
return client.chat.completions.create(**kwargs)
except RateLimitError as err:
if attempt == MAX_ATTEMPTS:
raise # 放弃路径:抛出错误,不要无限循环
retry_after = err.response.headers.get("retry-after")
if retry_after is not None:
# 服务端已明确说了容量何时恢复——不需要抖动
time.sleep(min(float(retry_after), MAX_DELAY))
else:
delay = min(BASE_DELAY * 2 ** (attempt - 1), MAX_DELAY)
# full jitter:把同步失败的客户端错开
time.sleep(random.uniform(0, delay))
response = chat_with_backoff(
model="openai/gpt-5.5",
messages=[{"role": "user", "content": "ping"}],
)
五次尝试、30 秒封顶、带抖动的延迟、尊重 Retry-After、最后干净地失败。整个模式就这些——忍住别把它写得更"聪明"。
成因二:撞上了网关对 Key 或账户的限额
每把 Router One API Key 都有每分钟请求数限额(rateLimit)和每分钟 token 限额(tokenLimitTpm),同一账户的所有 Key 还共享账户级的请求与 token 限额,其中包括单个模型的请求限额。这些限额按平台默认值生效——正常付费使用不设低额度固定上限——作用是不让泄漏的 key 或失控的循环把整个账户的吞吐打满。它们和 maxSpend 不同:消费硬上限由你在 Dashboard → API Keys 里按 Key 设置,而吞吐限额不在控制台修改。每客户一把 key 的转售模式依靠的是按 Key 的消费上限,吞吐限额在它下面兜底。
识别特征:响应会点名是哪一种限额。网关返回的 429 带 X-RateLimit-Scope——api_key 是这把 Key 的限额,subject 是整个账户的限额,subject_model 是账户在单个模型上的限额——同时带 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset 和 Retry-After;错误码为 RATE_LIMIT_EXCEEDED(请求数)或 TOKEN_QUOTA_EXCEEDED(每分钟 token 数)。这类 429 是确定性的:不管几点,都在同一个频率上复现。每分钟 token 限额在请求开始时按估算值(提示词长度加 max_tokens)计入,结算后再按实际用量修正,所以一批大提示词可能在实际用量达到之前先触到它。(如果 key 停摆时报的是 402,那是 maxSpend 上限或钱包余额的问题——钱的事,不是速度的事,速查页写了两者的区别。)
申请调高之前先问一句:为什么会撞上?如果是某个后台任务把生产流量共用的 key 打满了,更好的修法是再开一把 key,把吵闹的负载隔离出去、单独可见——但同一账户的 Key 仍共享账户级限额。负载确实需要更高吞吐时——压测、批量回填、中转站的汇总流量——Key 与账户限额都可以申请调高:发邮件到 support@router.one,写明账户 ID、Key、模型,以及预计峰值的每分钟请求数与 token 数。网关限额之上仍受上游容量约束。
成因三:insufficient_quota——套着 429 外衣的账单问题
如果你直连官方 OpenAI API,错误体里写着 insufficient_quota,请立刻停止重试。状态码虽然是 429,但它跟限流毫无关系:你的预付账单额度用光了,这个"限制"不会在几秒、几分钟后重置——账单不动它永远不动。每一次重试都是白打的请求,退避只是让你浪费得慢一点。
这个报错来自 OpenAI 平台,不是网关。insufficient_quota 修复指南完整讲了成因和解决路径,包括绑了支付方式但额度依然为零的情况。
怎么判断撞上的是哪一种
靠客户端症状猜不可靠,请求 Trace 才可靠。在 Router One 上,Dashboard → Logs 展示每个请求的最终记录——状态、模型、Token、花费、延迟与请求 ID——可按模型和时间范围过滤。诊断归结为三步:
- 先读错误体。
insufficient_quota会自报家门。看到这个字符串就是账单问题——走成因三。 - 再读
X-RateLimit-Scope。api_key、subject或subject_model就是成因二——网关对 Key 或账户的限额,X-RateLimit-Limit告诉你天花板在哪;upstream_provider指向成因一。 - 最后看模式。 上游限流(成因一)是突发的、和负载高峰相关——429 集中在特定时间窗,然后自行消散。网关限额则是平直、可预测的。
同一份每请求记录还能回答后续问题——那段故障窗口的重试白烧了多少 token——这就进入成本追踪的范畴了。
重试风暴:天真的重试如何把限流越搞越糟
限流是系统满载的信号。天真的响应——立刻重试、无限重试——把这个信号变成了放大器:每个被拒的请求变成两个,再变成四个,总到达率恰好在系统最需要降压的时刻不断爬升。这就是重试风暴,它能把服务在原始高峰早已过去之后继续按在水下。有上限的尝试次数和抖动(见上)是重试侧的防御。
结构性的修法是并发整形:用信号量或固定 worker 池限制同时在途的请求数,而不是每条任务起一个并发、让退避去收拾残局。8 个 worker 消化 10,000 条任务的队列,产生的是一条平稳可预测、天然低于限流线的请求速率;同样 10,000 条任务一次性并发发出,得到的是一堵 429 墙加一波同步重试潮。退避是安全带,并发上限才是把车速控制在能活命的区间。
网关故障转移在哪一层帮上忙
走 Router One 调用,在错误到达你之前多了一层:符合条件的 429 或 529,在存在另一个提供同一请求模型的健康供应商时,可以在那条路由上重试。限定词很重要——并非每个 429 都可重试(网关对 Key 或账户的限额、insufficient_quota 这类账单故障都不在候选之列),故障转移也不是零停机保证:没有兼容路由完成请求时,应用仍会收到错误。所以客户端的退避循环无论如何都要保留;故障转移的判定逻辑另有专页。
至于 Router One 自己的限流口径:正常付费用量没有低固定上限——但滥用防护、账户级保护限制和上游约束仍然存在。如果正当的负载撞上了天花板,那是找支持团队聊的事,不是架构问题。
常见问题
怎么判断 429 来自我的 Key 限额还是上游供应商? 读 X-RateLimit-Scope 响应头。api_key、subject 或 subject_model 表示网关对这把 Key、整个账户或账户在单个模型上的限额,错误码为 RATE_LIMIT_EXCEEDED 或 TOKEN_QUOTA_EXCEEDED;upstream_provider 表示模型厂商的限制。上游 429 是突发的、跟负载高峰相关、会自行消散;网关限额则在同一频率上确定性地复现。Dashboard → Logs 里能看到每个请求的状态和时间。
每个 429 都应该重试吗? 不。只有限制真的会重置的情况——上游限流——才值得用有上限的指数退避加抖动去重试。网关限额每分钟重置,按 Retry-After 节奏重试可以排队通过,但持续高于限额的流量会一直失败,直到负载降下来或限额调高;OpenAI 的 insufficient_quota 是账单额度耗尽,任何重试节奏都填不回余额。
Router One 会对我的请求加自己的限流吗? 正常付费用量没有低固定上限,但每把 Key 和每个账户都按默认的每分钟请求数与 token 数限流,上游供应商约束也仍会生效。这些限额不在控制台修改,可以发邮件到 support@router.one 申请调高,写明账户 ID、Key 和预计峰值流量。
只做指数退避够吗? 必要但不结构性。退避处理的是单个失败请求,拦不住一个无界批量任务从源头制造 429。用信号量或固定 worker 池限制在途并发,让总请求速率从构造上就低于限流线,退避留作安全网。
网关故障转移能让 429 彻底消失吗? 不能。符合条件的 429 或 529 在存在另一个提供同一请求模型的健康供应商时可以重试,这能吸收相当一部分上游限流事件——但并非每个 429 都可重试,这也不是零停机保证。应用仍应准备好用有上限的退避处理最终返回的 429。
排查清单
429 出现时:先读错误体(insufficient_quota 等于账单问题),再读 X-RateLimit-Scope(网关对 Key 或账户的限额——分散负载,或申请调高),最后才当作真正的上游限流处理——有上限的退避加抖动、尊重 Retry-After、并发封顶。其余状态码见错误码速查页,Key 与响应头的完整说明见 API 文档。换一个 base URL,你就有了让诊断从猜测变成查表的请求 Trace。