要点
429 表示触发了某一项限额:账号每分钟发出的请求超过 120 次,或排队中和处理中的任务超出 100 个,或用同一个密钥轮询同一个任务的间隔短于 5 秒。请在自己这一侧排队,让请求数保持在限额之内;重试时至少等待 Retry-After 秒,并沿用同一个幂等键;想知道图片何时就绪,就用 Webhook 代替频繁轮询。
429 是什么意思?
429 说明请求太频繁,或者同时进行的任务太多:API 会对新的请求限流,直到有额度空出来。请求频率和任务数都按账号计算,而不是按密钥:所有密钥共用同一份额度,多加一个密钥并不会提高它。
| 限额 | 默认值 | 错误码 | 来源 |
|---|---|---|---|
| 速率限制 | 每分钟 120 次请求 | ERR_RATE_LIMIT | 文档:限额 |
| 进行中的任务 | 排队中和处理中的任务共 100 个 | ERR_QUOTA_EXCEEDED | 文档:限额 |
| 任务轮询 | 同一个密钥对同一个任务,每 5 秒最多一次 | ERR_RATE_LIMIT | 文档:限额 |
| 参考图上传 | 自行注册且尚未充值的账号,每天 100 张;首次充值后没有此限制 | ERR_RATE_LIMIT | 文档:限额 |
| 访问错误 | 同一个 IP 地址每分钟 30 次 401 或 30 次 403(分别计数);限制最长 120 秒 | ERR_RATE_LIMIT | 文档:限额 |
这些是所有账号的默认值,刚注册的账号也是如此。你的账号的限额可能不同:如果不够用,请联系技术支持,并说明账号和所需的量。
计入速率限制的有:启动生成、上传文件、读取模型目录和读取余额。轮询任务不在其中,它按自己的规则单独计算。任务只要还在排队或处理中,就一直占用 100 个名额中的一个。
还有两种 429 和请求频率无关:一种是参考图的每日上传限额,首次充值后就没有这个限制;另一种是访问错误后的临时限制,这时 429 响应中的 details.reason 是 burst_block,该 IP 地址会被暂时限制,最长 120 秒。
区分不同的 429
状态码相同,原因却不止一种。请看 error.code 和 error.details,下一步怎么做由它们决定;error.message 是给人看的英文文本,措辞可能变化,不要用程序去解析它。
HTTP/1.1 429 Too Many Requests Retry-After: 17 { "error": { "code": "ERR_RATE_LIMIT", "message": "rate limit exceeded", "details": { "limit": 120, "window": "1m", "retry_after": 17 } } }
HTTP/1.1 429 Too Many Requests Retry-After: 2 { "error": { "code": "ERR_QUOTA_EXCEEDED", "message": "concurrency limit reached", "details": { "limit": 100, "scope": "account" } } }
第一个响应里,details.limit 和 details.window 指出触发的是哪项限额,Retry-After 响应头给出需要等待的时间。第二个响应里,details.scope = "account" 表示账号的名额已被任务占满:请等已有的任务完成,再放慢新任务的发送。
同时发送多少请求 才合适?
限额由两个数字决定:整个账号最多 100 个任务在排队或处理中,每分钟最多 120 次请求。其中占用多少由你决定,规则很直接:在自己这一侧排队,名额空出来再启动新任务,不要一次全部发出。
- 按整个账号计算:同一个账号上跑着多个服务时,它们共用这份限额。给每个服务分一部分,并为重试留出余量。
- 按事件空出名额:任务变为
completed或failed后,再启动下一个。图片就绪的通知可以用 Webhook 接收,见下文。 - 让请求分散在这一分钟里:每分钟 120 次平均下来是每秒 2 次,一连串请求会很快用掉这一分钟的额度。
遇到 429 后怎样重试?
规则是这样的:等待时间不要短于 Retry-After 秒(429 和 503 响应都带有这个响应头),再加一点随机延迟,避免各个进程同时重试,并限制重试的次数。重试时保留请求体、API 密钥和 X-Idempotency-Key:重发同一次提交,就不会创建第二个任务,也不会再扣一次费。
- 新图片用新的幂等键:重试同一个请求时沿用原来的键。
idempotency_in_flight:第一次提交还在处理中。请等待Retry-After秒,再用同一个幂等键重试。- 轮询:等待时间不要短于
details.min_interval_seconds与Retry-After中较大的那个,因为响应头有时给出的间隔更短。 burst_block:停止重试,先排除出现401或403的原因(密钥错误、账号被停用),到了Retry-After之后再继续。- 任务已经
failed:同一个幂等键不会重新启动它。再试一次要用新的幂等键,这是一次新的请求,有它自己的扣费;之前那一笔会自动退回。
下面的示例按这些规则重试。
# 需要 bash、curl 和 uuidgen。每个操作只创建一次幂等键。 IDEMPOTENCY_KEY="$(uuidgen)" || exit 1 curl -i --max-time 30 \ -X POST https://api.rendergrid.io/ api/ public/ v1/ images/ generate \ -H "Authorization: Bearer $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{ "model": "nano-banana-2.1", "prompt": "浅灰背景上,五个木方块排成笔直的一行", "resolution": "1K", "aspect_ratio": "1:1" }' # 如果返回 429,先读取 Retry-After 响应头,等待之后 # 再用同一个 $IDEMPOTENCY_KEY 重新发送这条完全相同的请求
import os import random import time import uuid import requests API = "https://api.rendergrid.io/ api/ public/ v1" def submit_with_backoff(payload, idempotency_key, max_attempts=5): key = os.environ["RENDERGRID_API_KEY"] headers = { "Authorization": f"Bearer {key}", # 所有尝试都使用同一个键 "X-Idempotency-Key": idempotency_key, } for attempt in range(max_attempts): response = requests.post( f"{API}/images/generate", headers=headers, json=payload, timeout=(10, 120), ) if response.status_code != 429: return response try: body = response.json() except ValueError: body = {} details = body.get("error", {}).get("details", {}) last = attempt + 1 == max_attempts if details.get("reason") == "burst_block" or last: # 临时限制和最后一次尝试交给你的代码处理 return response wait = int(response.headers.get("Retry-After", "2")) # 等待时间不短于 Retry-After time.sleep(max(0, wait) + random.uniform(0, 1)) payload = { "model": "nano-banana-2.1", "prompt": "浅灰背景上,五个木方块排成笔直的一行", "resolution": "1K", "aspect_ratio": "1:1", } response = submit_with_backoff(payload, str(uuid.uuid4())) print(response.status_code, response.text)
// Node.js 22+,文件扩展名为 .mjs import { randomUUID } from "node:crypto"; const API = "https://api.rendergrid.io/ api/ public/ v1"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function submitWithBackoff(payload, idempotencyKey, maxAttempts = 5) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch(`${API}/images/generate`, { method: "POST", headers: { Authorization: `Bearer ${process.env.RENDERGRID_API_KEY}`, "Content-Type": "application/json", // 所有尝试都使用同一个键 "X-Idempotency-Key": idempotencyKey, }, body: JSON.stringify(payload), signal: AbortSignal.timeout(120_000), }); if (response.status !== 429) return response; const body = await response.clone().json().catch(() => ({})); const last = attempt + 1 === maxAttempts; if (body.error?.details?.reason === "burst_block" || last) { // 临时限制和最后一次尝试交给你的代码处理 return response; } const wait = Number(response.headers.get("Retry-After") ?? 2); const delay = Math.max(0, wait); // 等待时间不短于 Retry-After await sleep((delay + Math.random()) * 1000); } } const response = await submitWithBackoff( { model: "nano-banana-2.1", prompt: "浅灰背景上,五个木方块排成笔直的一行", resolution: "1K", aspect_ratio: "1:1", }, randomUUID(), ); console.log(response.status, await response.text());
示例只重试 429,其他响应交给你的代码处理,遇到 burst_block 时则停止。5 次尝试是这个示例的设置,不是建议:文档里的另一个示例用的是 3 次。
怎样轮询任务 才不会收到 429?
同一个密钥对同一个任务,每 5 秒最多查询一次,而且只查到状态变为 completed 或 failed 为止:到了最终状态就不要再轮询。过早的轮询会得到 429 ERR_RATE_LIMIT。
文档里还有两条规则。一是限制总的等待时间:图片多久出来,文档没有承诺时限,它取决于模型和负载;等超时了,请保存任务的 id 稍后继续,你的客户端超时并不会取消任务。二是多个进程轮询时,让它们共用同一个间隔。想完全不轮询,就接入 Webhook。
怎样用 Webhook 代替 频繁轮询?
在每个需要通知的生成请求里传入 webhook_url。任务变为 completed 或 failed 时,我们会向这个地址发送一个带签名的 POST 请求,事件是 creation.completed 或 creation.failed。地址需要是可从互联网访问的 HTTPS 地址。
处理程序里要注意这几点:
- 验证签名:签名是对
X-Timestamp的值、一个点和请求体依次拼成的字符串计算的 HMAC-SHA256,放在请求头X-Webhook-Signature-256中。请在框架解析 JSON 之前,对原始字节计算。示例里时间戳的时间窗口是 300 秒,这是接收方自己的规则。 - 尽快应答:任何
2xx响应都表示“已收到”,我们最多等 10 秒。请先把事件可靠地保存下来或放进队列,下载图片留到之后。 - 做好重复投递的准备:如果没有收到响应,或响应不是
2xx,我们会重新投递:默认最多尝试 5 次,后四次分别在前一次之后的 60、120、240 和 480 秒,调度可能增加延迟。对每一对(id, event)只执行一次操作。 - 留着轮询当备用:投递不能百分之百保证;没收到 Webhook 时,用
GET /creations/{id}查询任务。
Webhook 去掉的是轮询,其他限额依然有效:启动请求的次数和进行中的任务数照旧计算。更详细的内容见讲批量生成、Webhook 和幂等键的文章和文档的 Webhook 一节。
任务完成后 会返回什么?
任务到达 completed 时,result_urls 中是成品文件的链接。文件只保存一天,所以请立即下载;轮询不会延长保存期限。
下面是 Nano Banana 2.1 在 1K 下的三张普通结果,每张一次尝试,附有提示词。它们是成功的输出,不是错误画面:画面里看不到 429,我们也没有测量生成它们时的耗时或负载。
提示词:五个方块排成一行
浅灰背景上横向排列五个相同木方块,间距一致,前侧面齐平。镜头略高于物件,左侧柔光投下整齐阴影。每个方块完整可见,两边留宽余白。不出现文字、标志、人物或水印。
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1,1K,1:1,一次尝试。这是一张普通的成功结果,不是错误画面。五个浅木色方块排成一行,高度相同,木纹清晰;桌面和背景都是浅灰色,两者在画面中部略偏上处有一条水平分界线。镜头略高于方块,每个方块的顶面都露出一条窄边。影子都向右落,与提示词要求的左侧柔光一致,最右一块的影子拖得最长,一直到画面右缘。最左和最右的方块在朝向中间的一侧多露出一道暗色窄边,所以看起来比中间的略宽。方块之间的间距看上去大致相等,只是最左和最右的两处缝隙略窄于中间两处。整排方块在画面中居中。两侧的边距各约占画面宽度的十分之一,比提示词要求的“宽余白”窄。画面中没有文字。
提示词:九只杯子排成三行三列
九只小白陶杯没有杯把,按三行三列整齐排列,正上方俯拍。各杯间距相同,杯口大小一致。哑光米色底,光线均匀柔和,杯底有细影,整组外围保留空白。不出现文字、标志、人物或水印。
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1,1K,1:1,一次尝试。这是一张普通的成功结果,不是错误画面。九只没有杯把的陶瓷杯排成整齐的三行三列,行距和列距看上去相同;整组位于画面正中,四周留有米色空白。杯子呈偏暖的米白色,而提示词写的是白色陶杯。杯口大小接近,轮廓略有差别。从高处向下拍,接近垂直,但每只杯子的右下侧都露出一弯外壁,所以不是严格的正上方视角。光线柔和,却来自左上方:影子都落在杯子的右侧和下方,不是提示词要求的均匀光,范围也比“杯底细影”大。画面中没有文字。
提示词:五个小罐排成弧线
五个相同的奶油色带盖小罐在沙色背景上排成平缓弧线,彼此等距。略微俯视,右侧柔光照出圆滑罐盖和独立阴影。所有罐子完整可见,弧线内侧保持空白。不出现文字、标志、人物或水印。
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1,1K,1:1,一次尝试。这是一张普通的成功结果,不是错误画面。五个奶油色的带盖小罐摆在沙色的颗粒状桌面上,排成一道中间高、两端低的弧线,相邻罐子的距离看上去相等。五个罐子都完整可见,盖子顶部各有一个圆形小钮,弧线内侧是空白。整组高于画面中心,下方留出大片空白。拍摄角度比提示词的“略微俯视”更陡,盖面呈明显的椭圆。影子都落在罐子的右侧,说明光从左边来,而提示词要求的是右侧柔光;相邻罐子的影子在罐脚附近相接,不是各自独立的影子。画面中没有文字。
这些做法 在哪些情况下不适用?
- 限额由整个账号共用:别的密钥和别的服务用的是同样的 120 次请求和 100 个名额,而你某个服务的队列看不到别人占了多少。请留出余量。
- 访问错误导致的限制与频率无关:遇到
burst_block,只等一会儿并不够,要先排除出现401或403的原因。 - 没有现成的重试时间表:文档给出的是规则,即不短于
Retry-After,再加一点随机延迟。文档里的示例有的尝试 3 次,有的尝试 5 次,你的服务该用几次,由你自己决定。 - 没有推荐的进行中任务数:限额是每个账号 100 个,实际占用多少取决于你的进程,文档没有给出推荐的数量。
- 这三张图什么也没测量:它们是普通的成功结果,不能据此判断速度或负载。
常见问题解答
429 错误是什么意思?
error.code 和 error.details。请等待 Retry-After 秒后再重试。收到 429 后,隔多久重试?
Retry-After 给出的秒数。再加一点随机延迟,限制重试次数,并用同一个 X-Idempotency-Key 重发相同的请求体。多加几个密钥能提高限额吗?
收到 429 这样的错误后,已扣的金额会退回吗?
来源
- Rendergrid 文档:限额默认限额、429 响应示例和带间隔的重试。
- Rendergrid 文档:错误码错误格式、
ERR_RATE_LIMIT和ERR_QUOTA_EXCEEDED、Retry-After响应头。 - Rendergrid 文档:任务与状态轮询频率和结果文件的保存期限。
- Rendergrid 文档:Webhook签名、重新投递和响应时限。
- Rendergrid 文档:重试不重复扣费幂等键和 429 之后的重试。





