6 月 4 号晚上,我给一个批处理任务接上 GLM-5.1。单线程跑得好好的,一开 8 个并发就间歇性报 429。往代码里塞 sleep(1),照样失败。
GLM-5.1 返回 HTTP 429 时,别急着给 sleep 加码。先保存响应体、响应头、Retry-After、请求时间、并发数和 Token 用量,判断撞上的是 RPM、TPM 还是并发上限。客户端则要配上指数退避、随机抖动、并发信号量和重试预算。
先放一张可以直接拿去用的判断表。
| 观察到的现象 | 更可能的限制 | 怎么验证 |
|---|---|---|
| 短请求也失败,错误集中在固定时间窗口 | RPM,请求数超限 | 统计滑动窗口内请求总数 |
| 长提示词比短提示词更容易失败 | TPM,Token 消耗超限 | 汇总输入、输出 Token |
| 并发从 8 降到 4 后明显缓解 | 并发连接上限 | 记录每次请求的 in-flight 数 |
| 单进程频率不高,依然随机 429 | API Key 被多个实例共享 | 汇总全部进程、容器和定时任务 |
| RPM、TPM、并发都不高 | 配额或账户级限制 | 对照响应体与控制台配额 |
截至 2026 年 6 月 5 日,公开素材里没有 GLM-5.1 固定的 RPM、TPM 和并发数字。限额可能跟套餐、项目、API Key,甚至账户有关。网上看到的“每分钟多少次”,别直接写死到生产配置里。
为什么会出现 HTTP 429
RFC 6585 §4 对 HTTP 429 的定义很直接:客户端在给定时间内发送了过多请求。标准状态行是:
HTTP/1.1 429 Too Many Requests
429 不等于余额不足。请求频率、Token 消耗、同时进行的连接数、共享 API Key 的聚合流量,以及账户配额,都可能触发它。
排查链路大致是这样:
flowchart TD
A[GLM-5.1 返回 429] --> B[保存响应体和全部响应头]
B --> C{存在 Retry-After?}
C -->|是| D[按服务端时间等待]
C -->|否| E[指数退避加随机抖动]
D --> F{检查压测日志}
E --> F
F --> G[RPM 接近阈值]
F --> H[TPM 随长请求升高]
F --> I[in-flight 并发过高]
F --> J[核对共享 Key 与账户配额]
麻烦就麻烦在这里:只看一句 429,根本不知道卡在哪一层。折腾半天后我认了,日志没留全,后面只能靠猜。
方案一:先把完整响应证据保存下来
先用最小请求确认问题。下面的地址和模型标识来自 2026 年 6 月 5 日收集并验证可运行的代码素材,实际使用前仍要对照平台当前文档。
import os, requests
url="https://open.bigmodel.cn/api/paas/v4/chat/completions"
r=requests.post(url,headers={"Authorization":f"Bearer {os.environ['ZHIPU_API_KEY']}"},json={"model":"glm-5.1","messages":[{"role":"user","content":"ping"}]})
print(r.status_code, r.headers.get("Retry-After"), r.text)
生产日志至少要保留这些字段:
| 字段 | 用途 |
|---|---|
| 请求开始时间、结束时间 | 还原限速窗口和耗时 |
| HTTP 状态码 | 筛选 429 请求 |
| 完整响应体 | 判断服务端给出的具体原因 |
| 全部响应头 | 查找 Retry-After 和配额线索 |
| 请求 ID | 交给服务端支持人员定位 |
| 模型名 | 避免多个模型日志混在一起 |
| 输入、输出 Token | 判断 TPM 压力 |
| 当前 in-flight 数 | 判断并发限制 |
| 进程、实例、API Key 标识 | 排查共享流量 |
请求 ID 对应哪个响应头,目前没有可靠素材能确认,我也不打算硬编一个字段名。最省事的做法是把全部响应头结构化保存,再按平台文档提取需要的字段。
压测时,我会聚合这几个指标:
R60(t) = 最近 60 秒内的请求数量
T60(t) = 最近 60 秒内的输入 Token + 输出 Token
C(t) = 当前尚未完成的请求数量
这里的 60 秒只是诊断窗口,不代表 GLM-5.1 官方一定按 60 秒计费或限流。
429 跟 R60 高度相关,更像 RPM;跟 T60 一起上涨,更像 TPM;C(t) 一超过某个范围就出现,更像并发限制。压测时一次只改一个变量,否则数据很快就会搅成一锅粥。
方案二:正确处理 Retry-After 和退避
RFC 9110 §10.2.3 规定,Retry-After 可以是非负整数秒,也可以是 HTTP-date。它不保证永远返回 "3" 这种随手就能解析的值。
使用 requests 时,可以把这件事交给 urllib3:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
s=requests.Session(); s.mount("https://",HTTPAdapter(max_retries=Retry(total=4,status_forcelist=[429],allowed_methods={"POST"},respect_retry_after_header=True,backoff_factor=1)))
后续用 s.post(...) 发请求。total=4 是客户端配置,不是 GLM-5.1 的建议值。上线前还得确认当前 urllib3 版本对 POST 和 Retry-After 的实际处理方式。
没有 Retry-After 时,我会用 Full Jitter:
import random
def wait_s(attempt, retry_after=None):
ceiling=min(30, 2 ** attempt)
return max(float(retry_after or 0), random.uniform(0, ceiling))
计算式如下:
等待时间 = max(Retry-After, random(0, min(30, 2^attempt)))
随机抖动不能省。假设 20 个 Worker 同时收到 429,又都固定等待 2 秒,它们会在 2 秒后整整齐齐地再冲一次。服务端看了估计也挺无语。
上面的自定义函数只适合 Retry-After 为秒数的情况。碰到 HTTP-date,要用标准日期解析器,或者直接让 urllib3 处理。我也不确定手写日期兼容逻辑算不算好办法,目前没找到比成熟库更稳的方案。
别忘了重试预算
无限重试会把一次限流变成流量放大器。我通常会同时卡住重试次数和总时间:
import time
started=time.monotonic()
def can_retry(attempt):
return attempt < 4 and time.monotonic()-started < 30
这里的重试预算是 4 次、30 秒,只用于演示,不是官方参数。实际值要看任务超时、队列长度,以及用户能等多久。
POST 也不天然代表业务幂等。请求可能已经处理成功,只是客户端没收到响应;这时再重试,可能产生重复结果或重复消耗。平台支持业务幂等键就传,没有这项能力,至少在本地给任务做去重。
方案三:用并发信号量削掉流量尖峰
单进程里最简单的做法是 BoundedSemaphore:
import threading, requests
limit=threading.BoundedSemaphore(4)
def post(url, **kwargs):
with limit: return requests.post(url, timeout=(10,120), **kwargs)
4 只是起始压测值,不代表 GLM-5.1 的官方并发上限。这里连接超时是 10000 ms,读取超时是 120000 ms,至少不会让失控连接一直占着信号量。
我一般从并发 1 开始,逐级测试 2、4、8,期间固定提示词和输出上限。每档记录这些数据:
- 总请求数与 429 数量
- 成功率和 in-flight 峰值
- 输入、输出 Token
- 请求开始和完成时间
到了多进程、多容器部署,线程信号量就管不住了。每个实例允许 4 并发,部署 10 个实例后,服务端看到的峰值就是 40。此时得用 Redis 令牌桶、集中任务队列或网关级限流。
如果项目还需要备用调用路径,可以同时评估 OpenRouter、Together AI 和 ofox.io 这类国际聚合平台;按 2026 年 6 月 5 日的产品资料,ofox.io 支持 OpenAI 兼容、Anthropic 原生和 Gemini 原生协议,并能按 Model、User、API Key 查看请求、Token、费用与失败记录,排查共享 Key 的聚合流量会省不少日志拼接工作。
但聚合平台不是“无限配额外挂”。上游模型有限额时,换一个 base URL 不会凭空消灭限制,还是要做客户端限流和有预算的重试。
怎么通过压测日志区分 RPM、TPM 和并发限制
我常用控制变量法。粗暴,但确实好用。
测 RPM
固定短提示词、输出上限和并发数,只逐步提高每秒请求数。如果 429 总在请求数量达到某个窗口后出现,而 Token 总量没怎么变化,RPM 的嫌疑最大。
测 TPM
保持每分钟请求数不变,把输入长度按 1 倍、2 倍、4 倍增加。长提示词越容易触发 429,错误又和滑动窗口 Token 总量相关,就该优先检查 TPM。
账本公式很简单:
窗口 Token = Σ(输入 Token + 输出 Token)
失败请求是否计入服务端限额,现有素材没有确认,不能自己脑补。客户端日志里要把“估算 Token”和“服务端返回 usage”分开记录。
测并发上限
固定请求频率和提示词长度,只调整 in-flight 数。并发 8 报错、并发 4 正常,而且 429 在连接建立后很快返回,就更像并发限制。
共享 Key 也得查。你本机每秒只发 1 次,不代表定时任务、CI、另一个容器和同事的脚本没在同时发。说实话,这种“幽灵并发”我已经踩过不止一次。
常见问题 FAQ
Q1:GLM-5.1 返回 HTTP 429,是不是账户没余额了?
不一定。HTTP 429 的标准语义是请求过多,可能由 RPM、TPM、并发数或 API Key 聚合流量触发。余额和套餐状态要去控制台核对,具体原因看完整响应体、响应头和配额信息。
Q2:GLM-5.1 收到 429 后,应该先检查什么?
先保存完整响应体、全部响应头、Retry-After、请求 ID、请求时间和模型名。线程数、进程数、实例数、in-flight 数和 Token 用量也要一起记。只留一句 429 Too Many Requests,基本等于没留。
Q3:为什么每秒只请求一次,GLM-5.1 仍然会报 429?
因为限额可能按 API Key、项目或账户聚合。多个线程、容器和定时任务共用凭证时,单个客户端看到的 1 RPM,不代表服务端看到的也是 1 RPM。
Q4:GLM-5.1 的 Retry-After 为空怎么办?
使用带随机抖动的指数退避,并设置最大等待时间、最大重试次数和全局并发限制。别固定 sleep(1),也别无限重试。429 持续出现时,先停掉压测,再核对服务端配额。
Q5:降低并发后还是一直报 429,怎么办?
继续检查 TPM、共享 API Key 和账户级配额。并发只是常见限制之一。长提示词触发率更高时,问题更可能出在 Token 窗口,不是连接数量。
我的最终选择
我现在的处理顺序已经固定了:先留全响应证据,再从日志里画出 RPM、TPM 和 in-flight 曲线。客户端限制并发,按 Retry-After 等待;没有这个响应头时,就用 Full Jitter。重试用 4 次或 30 秒这类预算兜底。
429 本质上是流量治理问题,给 sleep 后面多加几个零没用。连哪层限制被触发都没查清,重试越勤快,现场通常越热闹。
5195



被折叠的 条评论
为什么被折叠?



