429 Too Many Requests 怎么办:大模型 API 限流排查

博客 / 排障指南

429 Too Many Requests 怎么办:大模型 API 限流排查

调用大模型 API 返回 429 Too Many Requests,意思是你在某个时间窗口里用掉的配额超了,服务端暂时不接这个请求。它通常不是故障,也不是网络问题。处理顺序是:先看响应头和错误体,确认撞的是请求数(RPM)、token 数(TPM)还是账户额度;如果是前两种,按 Retry-After 或重置时间等一会儿再重试,同时在客户端加限速;如果是额度用完,重试多少次都没用,要去充值或换套餐。

通用的 429 解释文章大多只讲“请求太频繁”,但大模型 API 多了一个维度:token。一次请求塞进十万 token 的上下文,RPM 远没到上限,TPM 却可能一下就满了。下面按排查顺序展开。

429 有哪几种来源?

大模型 API 的 429 大致分三类,处理方法完全不同:

来源计量方式典型表现等待能否解决
请求数限额(RPM / RPD)每分钟或每天的请求次数高并发、小请求时出现,比如批量分类能,窗口过去就恢复
token 限额(TPM / ITPM / OTPM)每分钟输入、输出 token 数请求不多但上下文很长,比如 agent 读整个仓库能,但要控制单次请求大小
额度或余额耗尽账户余额、月度花费上限、订阅额度不管请求多慢都报 429,或错误信息提到 quota、billing不能,要处理账户

还有一类容易混进来:有些平台对整个组织或项目统一计数,同一个 API Key 被几台机器、几个同事同时用,各自看起来请求都不多,加起来已经超了。排查时要把同一组织下所有调用方算在一起。

怎么从响应头判断撞的是哪一个?

最直接的办法是把一次请求的完整响应头打印出来。下面的命令用 -i 输出响应头,请求体只要一个很短的输入,不会消耗多少额度:

# OpenAI 格式接口,$MODEL 换成你账户可用的模型名
curl -sS -i https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}" \
  | grep -iE '^(HTTP|x-ratelimit|retry-after)'

# Anthropic 格式接口
curl -sS -i https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d "{\"model\":\"$MODEL\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" \
  | grep -iE '^(HTTP|anthropic-ratelimit|retry-after)'

正常请求也会带这些头,所以不必等到报 429 才看。两家的头含义如下,具体字段以官方文档为准,各家会调整:

响应头含义
x-ratelimit-limit-requests / -tokensOpenAI:当前窗口的请求数、token 数上限
x-ratelimit-remaining-requests / -tokensOpenAI:窗口内还剩多少
x-ratelimit-reset-requests / -tokensOpenAI:多久后恢复,格式是时长,如 1s、6m0s、20ms
anthropic-ratelimit-requests-limit / -remaining / -resetAnthropic:请求数限额,reset 是 RFC 3339 时间戳
anthropic-ratelimit-input-tokens-*、-output-tokens-*、-tokens-*Anthropic:输入、输出 token 分开计,以及合计
retry-after建议等待的秒数,429 时最该看的一个

判断方法很简单:哪一项的 remaining 接近 0,撞的就是哪一项。remaining-requests 还剩很多而 remaining-tokens 是 0,就是 TPM;Anthropic 的输入 token 先见底,说明问题出在上下文太长,而不是输出太多。

错误体是第二个依据。OpenAI 格式的错误里,code 为 rate_limit_exceeded 是频率限额,insufficient_quota 是额度用完,后者虽然也是 429,但等待解决不了。Anthropic 的 429 错误体 type 是 rate_limit_error,message 会说明是哪类限额。Gemini 的 429 状态是 RESOURCE_EXHAUSTED,details 里可能带建议的重试间隔。聚合网关和中转服务会把上游的头原样透传、改写或者干脆去掉,所以经过网关时,先看网关自己的文档。

Retry-After 该怎么用?

原则是:有 Retry-After 就听它的,没有再用重置时间,两样都没有才用指数退避。几个容易出错的细节:

  1. 单位。Retry-After 按 HTTP 规范可以是秒数,也可以是一个 HTTP 日期。大模型 API 一般给秒数,但解析代码最好两种都支持。
  2. 值为 0 或很小。有时会拿到 retry-after: 0,立刻重试大概率还是 429。给一个下限,比如 1 秒,再加随机抖动。
  3. 头缺失。一些网关、代理或 SDK 封装会丢掉这个头。这时退回到 x-ratelimit-reset-*,再不行就指数退避:1、2、4、8 秒,封顶 60 秒左右,每次加 0 到 1 秒的随机量,避免多个进程同时醒来再一起撞墙。
  4. 重试次数要封顶。3 到 5 次后仍然 429,就不该继续重试,应该报出来,交给上层处理或者排队。
  5. 额度类 429 不重试。错误码是 insufficient_quota 或者信息里提到 billing、credit,直接失败并提示,不然只是在浪费时间。

下面是一段 Python 的处理函数,不依赖特定 SDK:

import random, re, time
from email.utils import parsedate_to_datetime

def parse_duration(s):
    """解析 OpenAI 风格的时长,如 '6m0s'、'1.5s'、'20ms'。"""
    total = 0.0
    for num, unit in re.findall(r"([\d.]+)(ms|s|m|h)", s or ""):
        total += float(num) * {"ms": 0.001, "s": 1, "m": 60, "h": 3600}[unit]
    return total or None

def wait_seconds(headers, attempt):
    h = {k.lower(): v for k, v in headers.items()}
    ra = h.get("retry-after")
    if ra:
        try:
            sec = float(ra)
        except ValueError:  # HTTP 日期格式
            sec = parsedate_to_datetime(ra).timestamp() - time.time()
        return max(sec, 1.0) + random.random()
    for key in ("x-ratelimit-reset-tokens", "x-ratelimit-reset-requests"):
        sec = parse_duration(h.get(key))
        if sec:
            return max(sec, 1.0) + random.random()
    return min(2 ** attempt, 60) + random.random()

def call_with_retry(send, max_attempts=5):
    for attempt in range(max_attempts):
        resp = send()
        if resp.status_code != 429:
            return resp
        body = resp.text.lower()
        if "insufficient_quota" in body or "billing" in body:
            raise RuntimeError("额度或余额不足,重试无效:" + resp.text[:200])
        time.sleep(wait_seconds(resp.headers, attempt))
    raise RuntimeError("连续 429,已放弃")

send 是一个发请求并返回 requests.Response 的函数。如果用官方 SDK,它们一般自带重试,先查清楚默认重试几次,避免自己的重试套在 SDK 的重试外面,次数翻倍。

怎么在客户端提前限速,而不是等 429?

重试只是兜底。批量任务更好的做法是按限额主动控速,让请求几乎不触发 429。因为大模型同时限请求数和 token 数,限速器也要两个桶一起算:

import threading, time

class DualBucket:
    """同时按每分钟请求数和每分钟 token 数限速。"""
    def __init__(self, rpm, tpm):
        self.cap = {"req": rpm, "tok": tpm}
        self.level = dict(self.cap)
        self.rate = {"req": rpm / 60, "tok": tpm / 60}
        self.t = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self, est_tokens):
        est_tokens = min(est_tokens, self.cap["tok"])  # 单次超过桶容量时避免死等
        while True:
            with self.lock:
                now = time.monotonic()
                for k in self.level:
                    self.level[k] = min(self.cap[k], self.level[k] + (now - self.t) * self.rate[k])
                self.t = now
                if self.level["req"] >= 1 and self.level["tok"] >= est_tokens:
                    self.level["req"] -= 1
                    self.level["tok"] -= est_tokens
                    return
            time.sleep(0.05)

bucket = DualBucket(rpm=50, tpm=40_000)   # 示例数值,按你账户的实际限额填
# 每次请求前:bucket.acquire(输入 token 估算 + max_tokens)

几个实际要注意的地方:

  • rpm、tpm 取实际限额的八到九成,给其他进程和估算误差留一点余量。
  • token 估算要包含 max_tokens。有的平台在请求开始时就按 max_tokens 预占输出额度,把 max_tokens 设得远大于实际需要,会白白吃掉 TPM。
  • 多进程或多机器要共用一个桶。单机内存里的桶管不住别的机器,可以放到 Redis 之类的共享存储里,或者把所有调用收到一个出口服务。
  • 用响应头校准。每次响应后读 remaining,如果它比你的桶估计的少很多,说明有别的调用方在共用额度。

怎么从根上减少 429?

控速之后还频繁撞 TPM,就该从请求本身下手:

  • 缩短上下文。agent 类工具容易把整个文件甚至整个目录塞进去。限制单次读入的内容,或者先摘要再提问。
  • 用提示缓存。重复的长系统提示可以缓存。按 Anthropic 文档的说明,部分模型的缓存读取 token 不计入输入 token 限额,具体规则和写法见 Claude API 提示缓存怎么用才省钱。
  • 把轻任务分到另一组模型。分类、格式化这类任务用轻量模型,它们通常有单独的限额,主模型的 TPM 留给难任务。思路见 多模型路由怎么做。
  • 申请提高限额。多数平台的限额随用量等级提升,控制台里可以看到当前等级和升级条件。走正规渠道申请,不要靠多开账号、轮换 Key 来叠加限额,这违反大多数服务条款。

429、529 和 503 有什么区别?

这一节单独列出来,是因为很多人把服务端过载当成自己超限,去改代码、降并发,其实问题不在自己这边。

状态码谁的问题怎么处理
429你的调用超过了分给你的配额按 Retry-After 等待,客户端限速
529Anthropic 用来表示服务端整体过载(overloaded_error)退避重试,看状态页;降低自己的并发作用不大
503服务暂时不可用,可能是过载、维护或网关后端异常退避重试,看状态页

区分的关键是响应头:429 时你的 remaining 通常已经见底;如果 remaining 还很充足却报 429,或者大批不相关的用户同时报限流,就更像是服务端在收紧或出了状况。完整的错误码含义和哪些该重试,可以对照 大模型 API 错误码速查。

怎么判断是不是很多人同时被限流?

自己的限额没问题却持续 429,可以这样查:

  1. 打开 cdnping 的 挂了吗 页面,找到对应服务,比如 Claude 或 ChatGPT。页面综合了官方状态页、每 5 分钟一次的 API 探测和用户一键报告,报告类型里有“限流”一项。如果最近一段时间“限流”报告明显变多,说明不只是你一个人。你也可以顺手提交一条。
  2. 看 API 状态 页,例如 /status/anthropic 和 /status/openai。它从 Cloudflare 边缘每 5 分钟探测官方 API 入口,并同步官方状态页事件,能看出入口本身是否异常。
  3. 读官方状态页时注意事件等级和受影响组件,“部分模型错误率升高”这类 minor 事件经常就表现为 429 或 529 增多。怎么读状态页可以参考 怎么看 OpenAI 状态页和 Claude 状态页。

如果你用的是 Codex 这类按订阅计量的工具,遇到的“用量上限”和 API 的 429 是两套机制,额度何时恢复要看平台公告,可以参考 Codex 额度重置时间线。

一份可以照着走的检查表

遇到 429 时,按顺序检查:

  1. 打印响应头和错误体,确认是 RPM、TPM 还是额度。
  2. 错误码是 insufficient_quota 或提到 billing:停止重试,去控制台查余额和花费上限。
  3. 有 retry-after:按它等,值为 0 时至少等 1 秒并加抖动。
  4. 没有 retry-after:读 x-ratelimit-reset-* 或 anthropic-ratelimit-*-reset,再不行用指数退避,封顶重试次数。
  5. 确认同一组织、同一 Key 下还有谁在调用,把它们算进总量。
  6. 检查 SDK 自带重试,避免重试叠加。
  7. 批量任务加双桶限速,token 估算包含 max_tokens。
  8. 限额充足却仍然 429:去 /down 看“限流”报告,去 /status 看探测和官方事件,确认是不是服务端的问题。

相关文章

继续阅读