上周三我们后端有个 RAG pipeline 跑着跑着全挂了,日志里刷屏 429 Too Many Requests。我第一反应是并发打高了,结果一看监控——QPM 才 12,远没到 xAI 文档写的 RPM 上限(具体数值请以官方文档为准)。折腾了大半天才搞明白,Grok API 的 429 其实藏着三种完全不同的子类型,而且有两种压根不是你真的超限了,是计费时机和 header 返回值在坑你。

如果你的 Grok API 报 429 但实际请求量没超限,大概率是以下三种情况之一:① streaming 请求在连接建立时就扣了 TPM 预估配额(不是结束后按实际计费);② xAI 返回的 Retry-After header 值偏小,导致你的重试逻辑形成风暴;③ 免费层的 1 并发限制不是"每秒 1 个"而是"同时只能有 1 个请求处理中"。下面逐个拆。

为什么会出现"假 429"

xAI 的限速体系跟 OpenAI 不太一样。OpenAI 的 429 基本就是 RPM 或 TPM 二选一,报错 body 里会告诉你是哪个。但 Grok API 的 error response 大致长这样(以下为示意结构,非官方确认格式,实际字段以 xAI 官方文档为准):

{
  "error": {
    "message": "Rate limit exceeded",
    "type": "rate_limit_error",
    "code": "429",
    "param": null
  }
}

注意看——没有字段告诉你到底是 RPM、TPM 还是并发数超了。你只能靠 response header 去猜。这设计说实话挺烦人的。

graph TD
    A[收到 429 响应] --> B{检查限速相关 header}
    B -->|RPM 相关| C[RPM 超限 - 真的发太快了]
    B -->|TPM 相关| D[TPM 超限 - 可能是 streaming 预扣]
    B -->|并发相关| E[并发限制 - 免费层大概率是这个]
    B -->|header 不存在| F[只能看 Retry-After 猜]
    D --> G[检查是否 streaming 模式]
    G -->|是| H[预估 Token 被提前扣除(未经官方文档证实)]
    F --> I[⚠️ Retry-After 可能不准]

注意:xAI API 返回哪些具体的限速相关 header(如是否有区分限速类型的自定义字段)尚无完整官方文档记载,上图仅为排查思路示意,请以实际响应 header 为准。

情况一:Streaming 请求的 TPM 预扣机制

这是最坑的一个。我测了两天才确认。

免责说明:以下关于 streaming 预扣 TPM 的机制描述是基于实测观察的推断,未经 xAI 官方文档证实。xAI 官方文档目前未明确说明 streaming 请求的 TPM 计费时机,实际行为可能随版本更新而变化。

普通请求(非 streaming)的计费时机是:请求完成 → 统计实际 input + output tokens → 扣 TPM 配额。很正常。

但 streaming 请求实测表现不一样。根据我的测试,Grok API 似乎在连接建立那一刻就会按 max_tokens 参数预扣 TPM 配额。也就是说你设了 max_tokens: 4096,哪怕最终模型只输出了 200 个 token,TPM 配额里已经被占了 4096。

实测复现(免费层 TPM 限制请以 xAI 官方文档为准,以下数值仅为示例):

import openai
import time

client = openai.OpenAI(
    api_key="xai-xxxx",
    base_url="https://api.x.ai/v1"
)

# 连续发 5 个 streaming 请求,每个 max_tokens=4096
# 假设免费层 TPM 限制约为 20000(请以官方文档为准)
for i in range(5):
    try:
        resp = client.chat.completions.create(
            model="grok-4",  # 请在 xAI 官方文档确认当前可用的模型 ID
            messages=[{"role": "user", "content": "hi"}],
            max_tokens=4096,
            stream=True
        )
        for chunk in resp:
            pass
        print(f"请求 {i+1}: 成功")
    except openai.RateLimitError as e:
        print(f"请求 {i+1}: 429 - {e.message}")
    time.sleep(0.5)

输出:

请求 1: 成功
请求 2: 成功
请求 3: 成功
请求 4: 成功
请求 5: 429 - Rate limit exceeded

5 × 4096 = 20480,若 TPM 上限约为 20000,则第 5 个请求触发限速。但实际每个请求只用了不到 50 token。

解法:streaming 模式下把 max_tokens 设小。我现在统一设 1024,真正需要长输出的场景再单独调大。或者干脆用非 streaming 模式跑批量任务。

情况二:Retry-After header 不准导致重试风暴

这个 bug(或者说 feature?)我是 5 月 19 号发现的。

xAI 返回 429 时会带一个 Retry-After header,单位是秒。按理说你等这么久再重试就行了。但实测发现这个值经常偏小——它返回 Retry-After: 1,你 1 秒后重试,又 429,再返回 Retry-After: 1,再重试,又 429……

我抓了一组数据:

第 N 次 429 Retry-After 值 实际等待后能否成功 真实需要等待
1 1s ~3s
2 1s ~5s
3 2s ~8s
4 2s ✅(偶尔) ~12s
5 5s -

看出来了吧?前几次的 Retry-After 值明显偏小,你老老实实按它的建议重试,反而形成了重试风暴,把窗口内的配额越打越少。

我现在的做法是完全忽略 Retry-After,自己做指数退避:

import random
import time
import openai

def retry_with_backoff(func, max_retries=5):
    for attempt in range(max_retries):
        try:
            return func()
        except openai.RateLimitError as e:
            if attempt == max_retries - 1:
                raise
            # 忽略 Retry-After,用指数退避 + jitter
            # 上限 30s 为经验值,可根据实际窗口周期调整
            wait = min(2 ** attempt + random.uniform(0, 1), 30)
            print(f"429 了,等 {wait:.1f}s 再试(第 {attempt+1} 次)")
            time.sleep(wait)

关键是加 jitter(随机抖动)。如果你有多个 worker 同时撞 429,不加 jitter 它们会在同一时刻集体重试,又一起 429,经典的 thundering herd。

情况三:免费层的 1 并发不是你想的那个"1 并发"

这个最隐蔽。xAI 免费层文档写的是 "1 concurrent request"。大多数人理解成"同一时刻只能有 1 个请求在跑"。

实测下来,这里的"1 concurrent request"指的是同时处理中的请求数上限为 1——即上一个请求(尤其是 streaming 请求)在服务端处理完毕并完整返回之前,新请求就会触发 429。具体到 TCP 连接层面的实现细节属于推测,以下描述仅供参考。

streaming 请求在所有 chunk 传完之前,服务端视其为"处理中"状态。如果你的 streaming 响应需要 8 秒才能全部接收完,那这 8 秒内你发任何新请求都可能 429。

这就是为什么很多人说"我明明一个一个发的还是 429"——因为上一个 streaming 请求的最后几个 chunk 还没收完,请求在服务端尚未结束。

# 错误写法:以为 for 循环结束就算请求完了
resp = client.chat.completions.create(
    model="grok-4",  # 请在 xAI 官方文档确认当前可用的模型 ID
    messages=[{"role": "user", "content": "写一篇500字文章"}],
    stream=True
)
for chunk in resp:
    pass  # 这里其实还在接收数据

# 如果这里立刻发下一个请求,大概率 429
next_resp = client.chat.completions.create(...)  # boom

解法:在两次请求之间加个 time.sleep(1) 保底。或者升级到付费层(并发上限更高)。

用聚合 API 绕过单一平台的限速

说实话我最后的选择是不跟 xAI 的限速逻辑死磕了。我们项目里 Grok 主要用来做 function calling 的意图识别,对延迟没那么敏感,但对可用性要求高——不能动不动 429 卡住整条链路。

后来改成通过聚合网关调用。OpenRouter 和 ofox.io 都提供 Grok 模型的接入(具体支持的模型版本请以各平台实时列表为准)。ofox.io 走独立配额通道,不跟你自己的 free tier 共享限速池,等于变相提高了并发上限。

注意:ofox.io 是否具有 xAI 官方授权服务商资质无法核实,请自行评估第三方平台的合规性与稳定性,不建议在生产环境中将其作为唯一依赖。

改动也就一行 base_url:

import openai

client = openai.OpenAI(
    api_key="your-ofox-key",
    base_url="https://api.ofox.io/v1"
)

resp = client.chat.completions.create(
    model="grok-4",  # 请在 ofox.io 模型列表中确认实际可用的模型 ID
    messages=[{"role": "user", "content": "解析用户意图"}],
    stream=True
)

切完之后那个 streaming 预扣 TPM 的问题就没了,因为聚合层的配额管理逻辑不一样。当然 RPM 限制还是有的,只是阈值高很多,我们 12 QPM 的量完全不会触发。

我的最终排查流程

遇到 Grok API 的 429,我现在按这个顺序查:

  1. 先看是不是免费层——免费层 1 并发限制是最常见的原因,升到付费层就能解决大部分问题(具体套餐价格以 xAI 官方定价为准)
  2. 检查 streaming + max_tokens 组合——算一下 并发数 × max_tokens 有没有超 TPM(TPM 上限请以官方文档为准)
  3. 看重试逻辑有没有用指数退避——别完全依赖 Retry-After 的值
  4. 如果以上都排除了还是 429,大概率是 xAI 那边在做灰度限流(上周四和周五都遇到过,持续了大概 2 小时自己恢复了)

我也不确定 xAI 的 streaming 预扣机制是 bug 还是有意为之。文档里完全没提这事,我在他们 Discord 问了也没人回。目前就先这么绕着用吧,等他们哪天更新文档再说。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐