Gemini API 429 配额超限:从报错原文到降速方案

429 的原文长什么样:先读三行信息

RESOURCE_EXHAUSTED 是配额类报错的总称

Gemini 撞到配额上限时,返回体固定是这一套:

{
  "error": {
    "code": 429,
    "message": "Resource has been exhausted (e.g. check quota).",
    "status": "RESOURCE_EXHAUSTED"
  }
}

code 是 429,statusRESOURCE_EXHAUSTED,这两行说明请求本身没写错、key 也没问题,纯粹是单位时间内的量超了。它和你代码里的 bug 无关,所以别去改 prompt,改的是发送节奏。

详细版本会指出具体是哪条配额

配额策略配置得比较细的项目,message 里会直接点名:

{
  "error": {
    "code": 429,
    "message": "Quota exceeded for quota metric 'Generate Content API requests per minute' and limit 'GenerateContentRequestsPerMinutePerProjectPerRegion' of service 'generativelanguage.googleapis.com'",
    "status": "RESOURCE_EXHAUSTED",
    "details": [
      {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "17s"}
    ]
  }
}

RetryInfo 里的 retryDelay 是服务端算出来的等待时间,按它来退避比拍脑袋定值靠谱得多。

有的客户端只给你翻译过的一句话

OpenAI 兼容客户端往往只显示 Rate limit reached 或者 当前分组上游负载已饱和。看到这类提示,要回到原始响应里翻 status 字段,或者直接在命令行复现一次,才能确认到底是不是 429。

三种配额维度,撞哪一个处理方式都不同

RPM、TPM、RPD 各管一段

区分的办法很简单:立刻重试就能过是 RPM;隔几分钟才缓过来是 RPD;只有长 prompt 批量任务才红是 TPM。

三个维度独立计数,撞任意一个都返回同样的 429

这就是为什么换了 key 还是红——同一个项目下的多把 key 共享同一个配额池,真正分开计算的只有不同项目。多 key 轮询能兜住的只有短时并发,救不了 RPD 撞满的情况。

并发数不是你想设多少就设多少

线程池开 50 个并发去丢请求,实际能通过的量由上游配额决定,多出来的部分会全部堆成 429。先算出目标速率(例如 RPM 上限是 200,就按每秒 3 个请求排队),再让线程数与之匹配,比事后加 sleep 有效得多。

定位到底撞了哪条:三步读数据

先看原始响应而不是客户端提示

curl -s -X POST \
  "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent" \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contents":[{"parts":[{"text":"ping"}]}]}' | jq '.error'

.error.message.error.details 一起打出来,就知道是每分钟还是每天被卡住。

再算清楚你的真实用量

抓一段时间的请求日志,按分钟和按天分别聚合,得到峰值 RPM 和全天总量。批量任务不要只看平均值,峰值那几分钟才决定你会不会撞墙。

接着把目标速率写进代码

把算出来的每分钟上限乘以 0.7 到 0.8 作为安全余量,落到客户端的限流器上。剩下的余量留给突发和重试,不然重试本身就会把配额吃干净。

客户端限流:信号量加队列的写法

用信号量把并发压到配额以内

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key="你的平台KEY", base_url="https://xyuapi.top/v1")
sem = asyncio.Semaphore(4)          # 同时不超过 4 个在飞
interval = 0.25                      # 每个请求的间隔下限,约 240 RPM

async def call_once(prompt: str, model: str = "gemini-2.5-pro"):
    async with sem:
        await asyncio.sleep(interval)
        r = await client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            timeout=180,
        )
        return r.choices[0].message.content

并发和间隔两个参数一起调,比只调其中一个更稳。信号量管住瞬时并发,间隔管住长期速率。

把长任务拆成可续跑的批次

两千条数据一次跑完,中途撞了 429 就得从头来。改成按 200 条一批,每批跑完写一次进度文件,重跑时先读进度文件跳过已完成的,整个任务就从「要么全成要么全废」变成了可中断可续跑。

给失败项留一个单独的池子

成功写结果、失败写待重试队列,等主流程跑完再统一处理待重试的少量条目。这样主流程不会被个别慢请求拖死,重试也不会把刚缓过来的配额又打满。

待重试队列本身也要限速,它和主流程共用同一个并发信号量,才不会在主流程刚缓过来的时候又被重试挤爆。

进队列的条件要写清楚:429、503、超时进队列;400 参数错误、401 鉴权失败直接丢弃。参数写错重试一百次也是同样的结果,只会白白吃调用次数。

再加一块用量看板,把每分钟请求数画成曲线,撞墙的时间点一眼可见,调完参数能立刻验证效果,比凭感觉调参可靠得多。

退避策略:指数退避必须带抖动

固定 sleep 的代价很高

一批线程同时撞墙,同时 sleep(5),5 秒后同时醒来再同时撞墙,永远在同一个节拍上打转。退避必须带随机抖动,把大家的重试时间错开。

import random, time

def retry_with_backoff(fn, attempts: int = 6, base: float = 1.0, cap: float = 60.0):
    for i in range(attempts):
        try:
            return fn()
        except Exception as e:
            if "429" not in str(e) and "RESOURCE_EXHAUSTED" not in str(e):
                raise
            wait = min(cap, base * (2 ** i)) * (0.5 + random.random() * 0.5)
            time.sleep(wait)
    raise RuntimeError("重试次数用尽")

优先采用服务端给的 retryDelay

返回体里有 RetryInfo 时,直接用它给的值加一点抖动,通常比指数退避算出来的更贴近真实窗口。只有拿不到 details 的时候,才退回纯本地退避。

退避要设上限和熔断

连续三次以上 429 就该停下来,把并发降到一半再继续,而不是无休止重试。重试是有成本的:它吃配额、吃时间,还可能把一次小拥堵放大成整体不可用。

减少请求数:三个比调参更管用的思路

合并请求

同一批要处理的内容,能合进一次调用就别拆成十次。合成一条带编号的清单,让模型一次输出十条结果,再按编号切分。请求数直接降到十分之一,配额问题大半消失。

合并的时候把输出格式写死:每行以编号开头,字段之间用固定分隔符。解析时按行切分再按编号对齐,哪一条缺失一目了然,重跑时只补缺的那几条即可。

注意别把合并做过头。一次塞进去五十条,输出长到触发长度上限,反而会出现截断和错位。实践上二十到三十条一批是比较稳的区间,具体看单条内容的长度。

加一层结果缓存

翻译、分类、摘要这类任务,同样的输入重复出现的概率很高。把输入的哈希当 key,结果落本地文件或 SQLite,命中就直接返回,既省配额也快。

用便宜模型打底,贵的模型兜底

先用 gemini-2.5-pro 跑全量,只把模型自己标记为「不确定」的少量样本交给 gemini-3.1-pro-preview 复核。这样既保住质量,又把高配额消耗的请求数压到很低。

这个思路在成本上也算得清:gemini-2.5-pro 单次 0.031 元,gemini-3.1-pro-preview 单次 0.09 元。一千条数据全部走后者接近九十元,先用前者全量跑、再挑出两成复核,总支出能压到原来的三成上下,而多数抽取类任务的质量差异小到看不出来。

复核提示词也要设计好:明确告诉模型只做判断、只输出结论与依据,不要重写全文。输出短、速度快、花费低,批量处理也更容易。

把复核结果落库,积累一段时间后回看被标记的样本,通常会发现打底模型的薄弱点集中在某一两类输入上,改这几类输入的提示词就行,不用整体推翻。

429 和其他状态码的区分

一张表分清谁是谁

状态码与字段常见 message 片段真实含义处理动作
429Resource has been exhausted配额撞顶退避等待,降低并发
429Quota exceeded for quota metric ... per minuteRPM 或 TPM 超限按秒级节流
503The model is overloaded上游临时过载换模型或稍后重试
504Deadline exceeded请求超时被中断加长超时并开流式
402 / 403额度或权限提示余额不足或权限未开检查账户余额与模型权限

503 不等于 429

上游过载返回的 503 会自己恢复,退避一两秒再试就行;429 撞的是你自己的配额节奏,不降速就会一直红。两者的处理方式恰好相反,混为一谈只会白等。

别用重试掩盖设计问题

如果每天固定时间大面积 429,那就不是运气问题,而是任务调度把高峰叠在了一起。错峰排班比加更多重试有效。

错峰的做法很具体:把批量任务挪到业务低谷时段,或者按项目把任务切成几个时间片,每个时间片单独限速。调整一次调度表,往往比在代码里多加十行重试逻辑都管用。

还要盯住客户端自带的自动重试开关。不少 SDK 默认会重试两到三次,再叠加你自己写的重试,一次失败实际会发出七八个请求,日志里看起来只是「偶发 429」,实际用量早就翻了倍。

聚合接入平台的配额与价格

接口信息

主入口 https://xyuapi.top/v1,备用 https://xyuai.cc/v1,另支持 Gemini 原生 /v1beta 路径。OpenAI 兼容协议,客户端里填 Chatbox、Cherry Studio、NextChat、Cline 都能直接用。

按次计费让成本可预估

平台按一次请求固定价收费,输入多长都是同一个价格。需要塞进长上下文的任务,成本不会因为输入翻倍而翻倍,做批量预算时按调用次数乘单价就能算出来。

模型价格适合的任务
gemini-2.5-pro0.031 元/次批量抽取、长文档总结
gemini-3-pro-preview0.05 元/次复杂推理、代码生成
gemini-3.1-pro-preview0.09 元/次高难度分析与复核
claude-sonnet-4-5-thinking0.09 元/次带思考链的长文处理
NanoBanana-Pro(生图)0.27 元/次图片生成与编辑

平台侧也能看到用量

出问题时先在平台的用量页面对一下自己的统计,两边数字差得离谱就说明有客户端在偷偷重试,把重试收敛住,用量自然会跌回预期。

上线前的检查清单

动笔改之前先做四件事

  1. 记下过去 24 小时的峰值 RPM 和全天总量,这是所有参数的依据。
  2. 把并发数压到配额八成以内,并给每个请求加上间隔下限。
  3. 退避函数带上抖动和上限,连续失败三次就自动降速。
  4. 任务写进度文件,中途断了能从断点接着跑。

跑通之后再优化

先用小批量(比如 50 条)把整套流程跑通,确认不再出现 429,再放大到全量。

留一份失败样本

把重试后仍然失败的请求单独存成文件,跑完回头看这批样本,多半能发现是某类超长输入或者某种特殊格式在拖后腿,修掉它比继续调参更值。

相关阅读

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

查看全部产品

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