GPT API 返回 429,意思只有一个:服务端认出你是谁了,但这一刻不想再接你的请求。它不是「服务器挂了」,也不是网络抖动,你重试一百次它也不会自己变好——除非你把发请求的节奏改对。先花两分钟搞清楚自己撞的是哪一种墙。
同一个 HTTP 429 状态码,背后至少三种完全不同的原因。第一种是速率限流:你这一侧请求太密,超过了每分钟请求数(RPM)或者每分钟 token 数(TPM)的上限。第二种是并发限制:这一分钟的总量其实没超,但同一时刻开着的连接太多,网关把多出来的那些直接挤掉了。第三种最容易被误判——你的额度或者余额已经用光,上游为了让客户端有点明确反馈,把它包装成了一个 429。
前两种是节奏问题,改代码能解决。第三种是钱的问题,改代码一点用都没有,你只会看到无限重试全部失败。
拿一个失败的请求,把响应头和响应体一起打出来。响应体里的 type 和 code 字段是分类的关键,insufficient_quota 和 rate_limit_exceeded 的处理方式差了十万八千里。响应头里的 retry-after 会直接告诉你服务端希望你等多少秒,比你自己猜的准。
第二步看时间分布。如果 429 集中在脚本刚启动的那几秒、或者集中在某几个 worker 身上,那是并发问题。如果每隔一分钟稳定冒出一批,那是 RPM 打满。如果从头到尾全是 429、一条成功都没有,基本可以判定额度已经见底。把每次 429 的时间戳和 retry-after 记进日志,一眼就能看出模式。
速率限流:读 retry-after,加退避重试,把并发从 10 降到 3。并发限制:加信号量,把同时飞出去的请求数量硬压住。额度耗尽:立刻停下所有重试,去后台查余额和用量,充值或者换 Key,顺手检查是不是有别的程序在用同一个 Key 跑批量任务。
OpenAI 系接口在每分钟请求数打满时,返回的原文大致长这样:
Rate limit reached for gpt-5.5 in organization org-xxx on requests per min (RPM): Limit 3, Used 3. Please try again in 20s.
注意 Limit 3 这个数字。很多新账号或低档位账号的 RPM 上限只有个位数,你觉得「才发了几次」,实际三次就用完了。句尾那句 Please try again in 20s 就是它给出的等待建议,通常和响应头里的 retry-after 一致。
同一句话里的关键词换成 on tokens per min (TPM),那就是按 token 量限流。这种情况在长文档处理、RAG 里拼接大段上下文、把整个代码仓库塞进 prompt 的场景里特别常见。RPM 才用掉两次,TPM 已经爆了,因为一次请求就吃掉了几万 token,后面九次请求全是白等。判断方法很简单:看报错里写的是 requests per min 还是 tokens per min。
这一段是最坑的,原始响应体是这样:
{"error":{"message":"You exceeded your current quota, please check your plan and billing details.","type":"insufficient_quota","code":"insufficient_quota"}}
HTTP 状态码同样是 429,但 type 是 insufficient_quota。你对着这个错误加退避重试,重试到天亮也不会有一次成功,只会把日志刷满。看到 insufficient_quota,正确的动作只有一件:去后台看余额。别动代码。
用 OpenAI 的 Python SDK 的话,traceback 里看到的通常是这两行:
openai.RateLimitError: Error code: 429
Error code: 429 - {'error': {'message': 'Rate limit reached...', 'type': 'requests'}}
type 是 requests 就是 RPM 撞墙,type 是 tokens 就是 TPM 撞墙,type 是 insufficient_quota 就是没额度了。捕获 openai.RateLimitError 的时候,把整个异常对象转成字符串写进日志,别只记一句「请求失败」。
假设你已经把 API Key 放在环境变量 KEY 里,直接跑这条:
curl -i -s -X POST https://xyuapi.top/v1/chat/completions \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","max_tokens":32,"messages":[{"role":"user","content":"hi"}]}'
-i 是关键,它会把响应头连正文一起打出来。只想看头不想看正文,把 -i 换成 -D - 再加 -o /dev/null。把这条命令连续跑十次二十次,记住第几次开始变成 429,这个数字就是你实际可用的 RPM 区间,比任何文档都可靠。
看到 retry-after: 20,就是让你等 20 秒。这个值比你拍脑袋写的固定等待精确得多,也可能短得多。代码里读到它就用它,读不到再退回去用指数退避自己算,两套逻辑留一个兜底就行,不要两个都硬编码。
x-ratelimit-limit-requests 是你这个 Key 每分钟的请求上限,x-ratelimit-remaining-requests 是当前窗口还剩几个。这两个数字能立刻回答「到底是不是我的配额本身太小」。如果 limit 就是 3,那问题不在你的代码,在账号档位,换个计费方式更快。
x-ratelimit-reset-requests: 1s 或者 6m0s 这种格式,表示窗口多久之后重置。6m0s 是六分钟之后才恢复,这时候你按一秒一次去重试纯属浪费,只会让用量记录更难看。把里面的 m 乘 60 换算成秒,再决定这次睡多久。
| type / code 特征 | 根因 | 你现在该做什么 |
|---|---|---|
type: requests,原文含 on requests per min (RPM) | 每分钟请求数打满 | 读 retry-after,加退避重试,并发从 10 降到 3 |
type: tokens,原文含 on tokens per min (TPM) | 单位时间 token 量超限 | 缩短单次上下文,长文档拆成多批,别一次塞满 |
type: insufficient_quota,code 同名 | 额度或余额已经耗尽 | 停下重试,进后台查余额和用量,充值或换 Key |
429 但没有任何 type,只有 retry-after | 网关层面的并发或连接数限制 | 加信号量压并发,worker 数量直接减半 |
| 429 且第一条请求就失败 | Key 或账号档位本身没有额度 | 先请求 /v1/models 确认 Key 可用,再看计费 |
同一个 Key,如果昨天跑得好好的、今天突然全是 429,先别怀疑代码,去看是不是有别的程序也在用这个 Key。多个项目共用一个 Key 是「莫名其妙被限流」最常见的原因:两边各自算着自己只有五个并发,加起来就是十个,谁都没超自己的账,服务端早就超了。给每个项目单独申请 Key,是成本最低的一次排查。
import random
import time
import requests
RETRY_STATUS = {429, 500, 502, 503}
def call_with_backoff(url, headers, payload, max_retries=5, base=1.0, cap=60.0):
for attempt in range(max_retries + 1):
resp = requests.post(url, headers=headers, json=payload, timeout=120)
if resp.status_code not in RETRY_STATUS:
resp.raise_for_status()
return resp.json()
if attempt == max_retries:
raise RuntimeError("重试 %d 次仍失败,最后一次状态码 %s" % (max_retries, resp.status_code))
server_wait = float(resp.headers.get("retry-after") or 0)
wait = server_wait or min(cap, base * (2 ** attempt))
wait = wait * (0.5 + random.random())
print("第 %d 次失败 status=%s,等待 %.1f 秒后重试" % (attempt + 1, resp.status_code, wait))
time.sleep(wait)
纯指数退避算出来的等待时间是个确定值,所有失败的 worker 会算出同一个数字,然后同时醒来、同时重发,形成一个整齐的请求尖峰,把刚缓过来的窗口再撞一次。乘以一个零点五到一点五之间的随机数,大家醒来的时刻就散开了。这是「越重试越挤」这个死循环的解药,不是可有可无的优化。
400 是请求格式写错了,401 是 Key 不对,403 是这个模型没给你开权限,404 是路径拼错了。这些错误你重试一万次结果都一样,只会白占线程和配额。503 和写着 overloaded_error 的可以重试,但等待时间要给得更长。
max_retries=5 配指数退避,最坏情况要等一分多钟。再加一个总耗时检查,单条请求超过九十秒就直接放弃,把它记进失败队列稍后补跑,别让整个批量任务卡死在一条请求上。
import asyncio
import httpx
SEM = asyncio.Semaphore(3) # 同一时刻最多 3 个请求在飞
async def one(client, payload):
async with SEM:
r = await client.post("/chat/completions", json=payload)
return r.json()
async def main(payloads):
limits = httpx.Limits(max_connections=3, max_keepalive_connections=3)
async with httpx.AsyncClient(base_url="https://xyuapi.top/v1",
headers={"Authorization": "Bearer sk-你的Key"},
limits=limits, timeout=120) as client:
return await asyncio.gather(*[one(client, p) for p in payloads])
一百条任务配三并发,跑完要的时间长一点,但成功率从三成变成九成九。反过来用 gather 一次性甩一百个请求出去,你收到的就是一屏 429 加一堆没写完的日志。生产脚本再包一层 try,失败的条目单独存文件,第二天只重跑那几条。
并发数不等于 RPM。信号量管的是「同一时刻几个在飞」,如果每个请求只要两百毫秒,三并发一秒钟就能发十五个请求,照样把每分钟三次的上限打爆。要严格控 RPM 就得用令牌桶:桶里按每分钟 N 个的速率补令牌,没有令牌就老实等着。简单实现:记录最近六十秒的时间戳,到上限就 sleep。
把一万条任务按每秒能承受的量切成小片,片与片之间留固定间隔。更稳的做法是丢进 Redis 队列或者一张数据库任务表,让固定数量的 worker 慢慢消费。分片还有一个好处:中途失败只需要重跑那一片,不用从头再来。
这两个客户端在设置里都有和并发相关的选项。Chatbox 打开「设置 → 高级」,把并发请求数从默认值降到 1 到 2。Cherry Studio 进「设置 → 模型服务」,把同一模型的并发上限压下来然后重启客户端。同时开着几个会话窗口轮流发消息等于把并发翻倍,测限流时先关掉多余窗口。
NextChat 在请求失败时会自动重试,短时间连着几次都失败就形成一次小爆发,反而拖长了被限流的时间。把重试次数调成 1,同时把请求超时调高一些,让它愿意多等一会儿。
这类编码客户端会在一次对话里连续发好几个工具调用请求,频率很高,改一个文件可能触发十几次请求。把「最大并行请求数」之类的选项从默认值降下来,长任务改成手动分段执行。同一台机器上一边开着 Cline、一边跑批量脚本,是最容易撞 429 的组合,测限流时先把其中一个停掉。
一份三万字的需求文档一次性塞进上下文,单次请求就可能撞 TPM 上限。按章节拆成五批,每批单独提问,再让模型汇总结论。拆批不只避开限流,模型面对结构清晰的输入也更靠谱。
按 token 计费的模型有个绑死的关系:你输入得越多、单次成本越高,同时也越容易撞 TPM 上限,长上下文天然又贵又容易被限。按次计费把这两件事拆开了——一次请求一个固定价,输入长度不影响价格。你不用担心一份长文档会把每分钟的 token 额度吃光,因为它根本不按 token 算你的钱,你只需要管住「一分钟发几次」。
| 模型 | 按次单价 |
|---|---|
| gemini-2.5-pro | 0.031 元/次 |
| deepseek-v4-flash-thinking | 0.05 元/次 |
| grok-4.1 | 0.05 元/次 |
| claude-sonnet-4-5-thinking | 0.09 元/次 |
| kimi-k2.6 | 0.09 元/次 |
| claude-opus-4-5-thinking | 0.12 元/次 |
| gpt-5.5 | 0.2 元/次 |
| claude-sonnet-4-7-thinking | 0.2 元/次 |
最低充值 7 元,按每次五分钱算能跑一百多次请求,足够把整条接入流程验证通再谈量。支付宝和微信直接付款,不需要海外信用卡,也不会有卡被风控导致额度清零这类连锁问题。
| 症状 | 原因 | 动作 |
|---|---|---|
| 第一条请求就 429 | 账号档位本身额度不足 | 进后台看用量,换按次计费或先充值 |
| 跑几分钟后开始 429 | RPM 或 TPM 打满 | 并发降到 3 以内,加退避重试 |
| 多台机器一起跑就 429 | 同一个 Key 被多个程序共用 | 拆成多个 Key,各程序单独用 |
| 单次请求 429 但很快恢复 | 网关瞬时并发挤压 | 加信号量,重试间隔加随机抖动 |
insufficient_quota 反复出现 | 余额或额度耗尽 | 立刻停止重试,去充值 |
| 客户端里偶尔冒 429 | 客户端并发设置过高 | 并发调到 1 到 2,关掉自动重试 |
小鱼API(xyuai.cc)是 AI API 接入平台,主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1,接口是 OpenAI 兼容的 chat/completions 和 models。任何支持自定义 OpenAI 地址的客户端——Chatbox、Cherry Studio、NextChat、Cline、LobeChat——把地址和 Key 填进去就能用,上面那些并发参数在客户端里也能改。选型上,日常问答和文档处理用 gemini-2.5-pro 或者 deepseek-v4-flash-thinking 就够,性价比高;写代码和长逻辑推理用 claude-sonnet-4-5-thinking;需要多模态理解或者复杂 agent 任务再上 gpt-5.5。先按最低七元充一笔,把限流参数调稳了,再决定要不要放量。