GPT API 报 429 rate limit 到底怎么办:先分清限流、并发还是额度用光,再动手改代码

429 的真实含义:你请求太密,或者额度已经用光

GPT API 返回 429,意思只有一个:服务端认出你是谁了,但这一刻不想再接你的请求。它不是「服务器挂了」,也不是网络抖动,你重试一百次它也不会自己变好——除非你把发请求的节奏改对。先花两分钟搞清楚自己撞的是哪一种墙。

先分清「太快」和「没钱」这两种 429

同一个 HTTP 429 状态码,背后至少三种完全不同的原因。第一种是速率限流:你这一侧请求太密,超过了每分钟请求数(RPM)或者每分钟 token 数(TPM)的上限。第二种是并发限制:这一分钟的总量其实没超,但同一时刻开着的连接太多,网关把多出来的那些直接挤掉了。第三种最容易被误判——你的额度或者余额已经用光,上游为了让客户端有点明确反馈,把它包装成了一个 429。

前两种是节奏问题,改代码能解决。第三种是钱的问题,改代码一点用都没有,你只会看到无限重试全部失败。

排查路径:先读头,再读 body,最后看时间分布

拿一个失败的请求,把响应头和响应体一起打出来。响应体里的 typecode 字段是分类的关键,insufficient_quotarate_limit_exceeded 的处理方式差了十万八千里。响应头里的 retry-after 会直接告诉你服务端希望你等多少秒,比你自己猜的准。

第二步看时间分布。如果 429 集中在脚本刚启动的那几秒、或者集中在某几个 worker 身上,那是并发问题。如果每隔一分钟稳定冒出一批,那是 RPM 打满。如果从头到尾全是 429、一条成功都没有,基本可以判定额度已经见底。把每次 429 的时间戳和 retry-after 记进日志,一眼就能看出模式。

三类 429 的处理动作完全不一样

速率限流:读 retry-after,加退避重试,把并发从 10 降到 3。并发限制:加信号量,把同时飞出去的请求数量硬压住。额度耗尽:立刻停下所有重试,去后台查余额和用量,充值或者换 Key,顺手检查是不是有别的程序在用同一个 Key 跑批量任务。

真实报错原文对照:你看到的是哪一类 429

RPM 限流:最典型的 429

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 一致。

TPM 限流:长上下文最容易踩

同一句话里的关键词换成 on tokens per min (TPM),那就是按 token 量限流。这种情况在长文档处理、RAG 里拼接大段上下文、把整个代码仓库塞进 prompt 的场景里特别常见。RPM 才用掉两次,TPM 已经爆了,因为一次请求就吃掉了几万 token,后面九次请求全是白等。判断方法很简单:看报错里写的是 requests per min 还是 tokens per min

额度耗尽伪装的 429

这一段是最坑的,原始响应体是这样:

{"error":{"message":"You exceeded your current quota, please check your plan and billing details.","type":"insufficient_quota","code":"insufficient_quota"}}

HTTP 状态码同样是 429,但 typeinsufficient_quota。你对着这个错误加退避重试,重试到天亮也不会有一次成功,只会把日志刷满。看到 insufficient_quota,正确的动作只有一件:去后台看余额。别动代码。

OpenAI SDK 抛出来的 RateLimitError

用 OpenAI 的 Python SDK 的话,traceback 里看到的通常是这两行:

openai.RateLimitError: Error code: 429
Error code: 429 - {'error': {'message': 'Rate limit reached...', 'type': 'requests'}}

typerequests 就是 RPM 撞墙,typetokens 就是 TPM 撞墙,typeinsufficient_quota 就是没额度了。捕获 openai.RateLimitError 的时候,把整个异常对象转成字符串写进日志,别只记一句「请求失败」。

第一条该敲的命令:curl -i 打出响应头

完整可复制的 curl 命令

假设你已经把 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:服务端直接告诉你等几秒

看到 retry-after: 20,就是让你等 20 秒。这个值比你拍脑袋写的固定等待精确得多,也可能短得多。代码里读到它就用它,读不到再退回去用指数退避自己算,两套逻辑留一个兜底就行,不要两个都硬编码。

x-ratelimit-limit-requests 和 remaining

x-ratelimit-limit-requests 是你这个 Key 每分钟的请求上限,x-ratelimit-remaining-requests 是当前窗口还剩几个。这两个数字能立刻回答「到底是不是我的配额本身太小」。如果 limit 就是 3,那问题不在你的代码,在账号档位,换个计费方式更快。

x-ratelimit-reset-requests 要换算成秒

x-ratelimit-reset-requests: 1s 或者 6m0s 这种格式,表示窗口多久之后重置。6m0s 是六分钟之后才恢复,这时候你按一秒一次去重试纯属浪费,只会让用量记录更难看。把里面的 m 乘 60 换算成秒,再决定这次睡多久。

三种 429 的区分表:type 和 code 决定动作

按 type 和 code 对号入座

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,是成本最低的一次排查。

指数退避 + 随机抖动:重试要这么写

完整可运行的 Python 重试封装

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 会算出同一个数字,然后同时醒来、同时重发,形成一个整齐的请求尖峰,把刚缓过来的窗口再撞一次。乘以一个零点五到一点五之间的随机数,大家醒来的时刻就散开了。这是「越重试越挤」这个死循环的解药,不是可有可无的优化。

只对 429 / 500 / 502 / 503 重试

400 是请求格式写错了,401 是 Key 不对,403 是这个模型没给你开权限,404 是路径拼错了。这些错误你重试一万次结果都一样,只会白占线程和配额。503 和写着 overloaded_error 的可以重试,但等待时间要给得更长。

最大重试次数和总耗时上限

max_retries=5 配指数退避,最坏情况要等一分多钟。再加一个总耗时检查,单条请求超过九十秒就直接放弃,把它记进失败队列稍后补跑,别让整个批量任务卡死在一条请求上。

并发控制:批量任务不能一把全丢出去

asyncio.Semaphore 卡住并发上限

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。信号量管的是「同一时刻几个在飞」,如果每个请求只要两百毫秒,三并发一秒钟就能发十五个请求,照样把每分钟三次的上限打爆。要严格控 RPM 就得用令牌桶:桶里按每分钟 N 个的速率补令牌,没有令牌就老实等着。简单实现:记录最近六十秒的时间戳,到上限就 sleep。

批量任务走队列分片

把一万条任务按每秒能承受的量切成小片,片与片之间留固定间隔。更稳的做法是丢进 Redis 队列或者一张数据库任务表,让固定数量的 worker 慢慢消费。分片还有一个好处:中途失败只需要重跑那一片,不用从头再来。

客户端侧怎么调:Chatbox / Cherry Studio / NextChat / Cline

Chatbox 和 Cherry Studio:先找并发设置

这两个客户端在设置里都有和并发相关的选项。Chatbox 打开「设置 → 高级」,把并发请求数从默认值降到 1 到 2。Cherry Studio 进「设置 → 模型服务」,把同一模型的并发上限压下来然后重启客户端。同时开着几个会话窗口轮流发消息等于把并发翻倍,测限流时先关掉多余窗口。

NextChat:关掉自动重试

NextChat 在请求失败时会自动重试,短时间连着几次都失败就形成一次小爆发,反而拖长了被限流的时间。把重试次数调成 1,同时把请求超时调高一些,让它愿意多等一会儿。

Cline / RooCode:把并行工具调用降下来

这类编码客户端会在一次对话里连续发好几个工具调用请求,频率很高,改一个文件可能触发十几次请求。把「最大并行请求数」之类的选项从默认值降下来,长任务改成手动分段执行。同一台机器上一边开着 Cline、一边跑批量脚本,是最容易撞 429 的组合,测限流时先把其中一个停掉。

长文档拆成分批喂

一份三万字的需求文档一次性塞进上下文,单次请求就可能撞 TPM 上限。按章节拆成五批,每批单独提问,再让模型汇总结论。拆批不只避开限流,模型面对结构清晰的输入也更靠谱。

按次计费为什么能绕开速率焦虑

TPM 对按次计费不成立

按 token 计费的模型有个绑死的关系:你输入得越多、单次成本越高,同时也越容易撞 TPM 上限,长上下文天然又贵又容易被限。按次计费把这两件事拆开了——一次请求一个固定价,输入长度不影响价格。你不用担心一份长文档会把每分钟的 token 额度吃光,因为它根本不按 token 算你的钱,你只需要管住「一分钟发几次」。

真实价格表

模型按次单价
gemini-2.5-pro0.031 元/次
deepseek-v4-flash-thinking0.05 元/次
grok-4.10.05 元/次
claude-sonnet-4-5-thinking0.09 元/次
kimi-k2.60.09 元/次
claude-opus-4-5-thinking0.12 元/次
gpt-5.50.2 元/次
claude-sonnet-4-7-thinking0.2 元/次

最低 7 元起充,支付宝微信都能付

最低充值 7 元,按每次五分钱算能跑一百多次请求,足够把整条接入流程验证通再谈量。支付宝和微信直接付款,不需要海外信用卡,也不会有卡被风控导致额度清零这类连锁问题。

速查表 + 落地配置

照着症状直接找动作

症状原因动作
第一条请求就 429账号档位本身额度不足进后台看用量,换按次计费或先充值
跑几分钟后开始 429RPM 或 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/completionsmodels。任何支持自定义 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。先按最低七元充一笔,把限流参数调稳了,再决定要不要放量。

相关阅读

🚀 想要立即使用?来小鱼API体验全系列AI模型

查看全部产品

支持Gemini / Claude / GPT / Grok / DeepSeek · 国内直连 · 支付宝/微信