Claude API 429 限流:怎么判断、怎么退避、怎么不再撞墙

429 是「太快了」,不是「不够了」

报错原文先认准

限流的响应体大致长这样:

{"error":{"message":"Number of request tokens has exceeded your per-minute rate limit","type":"rate_limit_error","code":"rate_limit_exceeded"}}

Anthropic 原生协议下常见的是 {"type":"error","error":{"type":"rate_limit_error","message":"..."}}。看到 rate_limit_error 就是 429 的领域,看到 overloaded_error 通常是 529 或者 503,那是上游自己扛不住,跟你的调用频率没关系。这两种错误的重试节奏不一样:429 要等你的窗口滑过去,过载要等上游缓过来。

429 和余额、权限没有关系

429 表示服务端认出了你,只是这一刻不想再接更多请求。余额不足会返回 402 或者额度提示,Key 无效是 401,模型没开通是 403。看到 429 去充值,等于在错误的环节上花钱。

报错类型状态码真实的含义正确的动作
rate_limit_error429你这一侧的请求太快退避、降并发
overloaded_error529上游算力吃紧等得更久再试
额度耗尽提示402余额或额度不够充值或换 Key
invalid_api_key401身份没通过查鉴权头和 Key
502 / 5045xx网关或上游断了少量重试,别猛冲

限流的三个维度要分清

一是每分钟请求数,二是同一时刻的并发连接数,三是上游对该模型的总负载。前者靠退避解决,中者靠排队解决,后者只能等。分不清维度,就会出现「我明明只发了几次也被限」这种困惑,因为你可能同时在跑十个并发。

时间窗口是滑动还是固定

固定窗口是每分钟清零一次,你在第 59 秒发满、下一秒继续发,看起来是正常的。滑动窗口算的是最近 60 秒内的累计量,任何时候发满都会被立刻拦下。多数实现用的是滑动窗口,所以「掐着整分钟发」这类技巧没有用,真正有效的手段只有一个:把并发压下来。

响应头里的限流信息比报错更有用

retry-after 是服务端给你的等待秒数

多数网关在 429 的响应里带上 retry-after,单位是秒。它比你自己猜的等待时间更准。先读它,读不到再退回指数退避。

两套头的名字要对上

头名字含义出现场景
retry-after建议等待的秒数429、503
anthropic-ratelimit-requests-limit每分钟请求上限Anthropic 原生协议
anthropic-ratelimit-requests-remaining当前窗口剩余请求数Anthropic 原生协议
anthropic-ratelimit-tokens-remaining当前窗口剩余 token 数Anthropic 原生协议
x-ratelimit-remaining-requests剩余请求数OpenAI 兼容协议
x-ratelimit-reset-requests窗口重置时间OpenAI 兼容协议

用 curl 直接看一眼响应头,比在代码里猜快得多:

curl -i -s -o /dev/null -D - https://xyuapi.top/v1/chat/completions \
  -H "Authorization: Bearer sk-你的Key" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-5-thinking","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}' \
  | findstr /i "ratelimit retry-after"

Windows 下用 findstr,macOS 和 Linux 换成 grep -i,作用是同一个:把限流相关的头单独拎出来看。

剩余量接近 0 就该主动减速

如果 x-ratelimit-remaining-requests 已经是个位数,说明窗口快满了。这时候主动降到一半并发,比撞上 429 再退避要划算得多,因为撞一次浪费的是一个完整请求的等待时间。

退避重试要带抖动,不要整齐地一起撞

指数退避加随机抖动

import random
import time
import requests

def call_with_retry(payload, key, max_retry=5):
    url = "https://xyuapi.top/v1/chat/completions"
    headers = {"Authorization": "Bearer " + key,
               "Content-Type": "application/json"}
    for attempt in range(max_retry + 1):
        resp = requests.post(url, headers=headers, json=payload, timeout=(10, 300))
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code in (429, 529, 503):
            wait = float(resp.headers.get("retry-after") or 0)
            if wait <= 0:
                wait = min(2 ** attempt, 30) * (0.6 + random.random() * 0.8)
            print(f"[429] 第 {attempt+1} 次退避 {wait:.1f}s")
            time.sleep(wait)
            continue
        resp.raise_for_status()
    raise RuntimeError("重试次数用尽")

2 ** attempt 让等待时间翻倍,random.random() 那一段是抖动,作用是让并发的多个任务不要在同一毫秒一起重试。没有抖动的话,十个并发任务会在同一时刻再次撞上去,形成规律的次次失败。抖动幅度取 0.6 到 1.4 倍之间,是够用的范围。

别把重试上限设得太大

三次到五次是合理区间。设成二十次,一旦上游真的挂了,你的程序会卡在里面出不来。另外,按次计费模式下每次重试都是一次真实请求,重试风暴会实实在在消耗额度,所以重试次数不只是性能问题,也是成本问题。

只对可重试的错误重试

401、403、400 这类错误重试一万次也是同样的结果。重试白名单只有三种:429、5xx,以及网络层的连接超时。其余错误直接抛出,让上层看到真实原因。

把重试过程写进日志

每次退避打印三样:第几次重试、等待多少秒、服务端返回的错误类型。日志一摆出来,「偶发一次就恢复」和「连续五次都失败」立刻分明。前者调并发就能解决,后者要去看上游状态,改自己的代码没有用。

并发控制:把请求排队,而不是一起往外扔

用信号量压住峰值

import asyncio, httpx

async def worker(sem, client, payload, key):
    async with sem:                     # 同时最多 N 个请求在飞
        r = await client.post(
            "https://xyuapi.top/v1/chat/completions",
            headers={"Authorization": "Bearer " + key},
            json=payload, timeout=httpx.Timeout(300, connect=10))
        return r.json()

async def main(payloads, key, concurrency=4):
    sem = asyncio.Semaphore(concurrency)
    async with httpx.AsyncClient() as client:
        tasks = [worker(sem, client, p, key) for p in payloads]
        return await asyncio.gather(*tasks, return_exceptions=True)

Semaphore(4) 的意思是同一时刻最多四个请求在场,第五个必须等前面一个结束。把并发从 20 降到 4,多数场景下总耗时不升反降,因为不再有大批请求排队失败再重试。

并发数从低往上试

场景建议起始并发观察指标
单用户交互式对话1不该出现 429
小批量文本处理2 到 4看剩余请求数
批量跑长上下文1 到 2看上游过载频率
带工具调用的多轮流程1看单次耗时

先按这个起点跑,出现 429 就减半,连续几轮都没有 429 再考虑加一。一次加到二十个并发,只会让你花时间在重试日志上。

批量任务要分批加间隔

一百条文本不要一次全发。分成每批十条,批与批之间留一秒,整体跑完的时间几乎一样,429 出现的概率低很多。批量任务还可以失败重跑,把失败条目单独收集起来二轮重试,比整批重来省额度。

流式请求遇到 429 的位置不一样

首包之前就被拦下

流式请求如果并发过高,429 会在 HTTP 状态码这一层返回,根本不会开始吐内容。这种最容易处理,走同样的退避逻辑即可。

已经吐了一半被切断

少数情况下连接建立成功、内容吐了一部分之后被切断,客户端会看到流提前结束。这时的处理原则是:已经收到的那部分内容保留下来,不要丢掉重新生成,否则用户会看到文字闪回重来。把已生成的内容作为上下文再请求一次,让它接着写,比从头来更省也更快。

重试前先判断有没有必要

如果已经收到了八成内容,剩下两成重试一次要付一次请求的钱,还不如把已收到的部分直接交给用户。判断标准很简单:内容够用就不重试。

客户端里的限流相关设置

Chatbox、Cherry Studio 这类客户端

这些客户端的请求是你手动点的,一个人用几乎撞不到 429。真出现了,先看是不是同时开了多个会话在自动生成,或者装了插件在批量发请求。把自动请求关掉,问题通常立刻消失。

编辑器插件的自动补全

Cline、Cursor 这类插件会在你打字时后台反复发请求,还带工具调用,一次任务可能发几十条。插件里通常有最大迭代次数的设置,把它从默认的高值调到合理范围,能明显降低触发限流的概率。

多台设备共用一个 Key

手机、电脑、服务器同时用一个 Key,各自的并发会叠加,从任何单台设备看都很正常,合起来就超了。排查时把这个因素算进去,或者在需要高频调用的场景单独建一个令牌。

别在同一台机器上跑多个客户端

浏览器开着网页版、桌面客户端也开着、编辑器插件还在后台自动补全,三份流量汇到同一个 Key 上。任何一份单独看都正常,合起来就超了。排查时先把不用的关掉,只留一份,是最快的验证方式。

429 出现时用户看到的是什么

界面卡住和报错弹窗的区别

不做重试的客户端会直接弹一句请求失败;做了重试却不给提示的客户端,界面就是长时间没动静。两种观感都不好。在重试期间给一句「请求较多,正在重试」的提示,用户的耐心会明显不一样,这是几行代码的事。

给用户一句能看懂的话

不要把 rate_limit_error 原文弹给用户。翻译成「当前请求较多,已自动重试」并带上进度,用户知道程序还活着,就不会反复点按钮。反复点击是 429 变得更频繁的直接原因之一。

前端不要自动重发用户操作

用户点一次按钮发一次请求就够了。把按钮在等待期间禁用,能避免用户手动制造出来的并发。这个小改动对降低限流的作用,往往比调参数更直接。

重试不要嵌套

重试要放在最外层,不要在业务逻辑里再包一层。嵌套两层的时候,两层重试相乘就是九次请求,日志里看起来只重试了两次,实际发出去的请求数会远超预期,也是 429 一直不退的一个隐蔽原因。

接入地址和按次计费

一个 Key 调全系列模型

小鱼API 是 AI API 接入平台,走 OpenAI 兼容协议,主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1。Chatbox、Cherry Studio、NextChat、Cline 这些客户端把地址和令牌填进去就能用。按次计费,一次请求一个固定价,输入多长都不改价,所以长上下文任务不需要为长度额外算账。

模型单次价格
gemini-2.5-pro0.031 元/次
deepseek-v3.2-thinking0.049 元/次
claude-sonnet-4-5-thinking0.09 元/次
claude-opus-4-5-thinking0.12 元/次

按次计费还带来一个排查上的好处:重试的成本是可预期的,不会因为一次重试把预算打穿。但这也不意味着可以放任重试,仍然要设上限。

429 处理清单

顺序动作完成标准
1看响应体类型确认是 rate_limit_error 而不是过载
2retry-after按服务端给的秒数等待
3加抖动退避并发任务不再同时重试
4压并发数起始并发降到 2 到 4
5批量任务分批批间留出间隔
6检查多设备共用高频场景单独建令牌

这套动作走完,429 会从「随机出现」变成「偶尔出现且能自动恢复」,程序不需要你盯着。

相关阅读

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

查看全部产品

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