Nano Banana 2 503 模型过载:有界重试、暂停与出图验收
Nano Banana 2 返回 503 时,先确认接口和 HTTP 状态,再在次数与总耗时预算内退避重试;预算耗尽就暂停或排队,收到图片数据并完成解码才算出图成功。
文章目录

Nano Banana 2 返回 503、UNAVAILABLE 或“模型过载”时,先保留实际接口、模型 ID 和完整 HTTP 响应,再做有次数上限、总耗时上限和随机抖动的退避重试。预算用完后暂停或排队,不让用户一直转圈。最后必须检查响应里的图片数据,并确认文件能解码、能打开;HTTP 200 或一段文字都不等于出图成功。
当前 Nano Banana 2 模型文档对应的图片模型是 gemini-3.1-flash-image。下文针对 Gemini Developer API 原生 generateContent 的生成和编辑流程。Gemini 应用、Vertex AI、第三方供应商的接口与错误约定需要分别确认,不能直接套用。
先分清:503、429、没图片,还是请求超时

先查看 HTTP 状态码,再读结构化错误字段。不要用 "503" in str(error) 或搜索“overloaded”来决定重试:错误说明里可能引用别的错误码,代理也可能返回自己的错误页。
| 实际信号 | 此时能判断什么 | 下一动作 |
|---|---|---|
| HTTP 503,收到明确错误响应 | 当前请求所经服务暂时不可用;可能是过载或维护 | 在预算内退避重试;保存响应来源 |
| HTTP 429 | 需要区分速率限制、配额耗尽及当前限制维度 | 速率限制可按提示等待;额度耗尽先停止,不进入本页的 503 循环 |
| HTTP 400、402、403、404 | 请求、支付前提、权限或资源存在其他问题 | 按完整错误修正原因,不原样循环重试 |
| HTTP 200,但没有可用图片 | 传输成功,图片任务仍未完成 | 查看候选内容、结束原因、阻止信息与解析逻辑 |
| 客户端超时、连接断开,没有确定结果 | 不知道服务是否已经完成生成 | 暂停并核对日志或已有任务结果,不自动重放 |
Google 的排错文档建议对瞬态错误采用退避、抖动和有限重试。但“200 无图”不能一律归因于安全拦截:输出配置、模型选择、候选结束原因和代码只读取文本,都可能改变判断。先看真实响应,不急着改提示词。
503 也不能单独证明“全球所有用户都受影响”。记录时间、接口域名、API 版本、模型、HTTP 状态、脱敏错误和 request ID(如果有)。第三方入口返回 503 时,要区分其网关错误与 Google 上游响应;影响范围不清楚,可先按 Nano Banana 2 宕机判断指南核对对应入口和状态页。
用当前图片请求恢复生成或编辑
图片请求应保持任务本身:生成时提供文字,编辑时提供文字和原图,输出仍要求图片。不能把 gemini-2.5-flash 等文本模型换上来,然后把返回文字当作恢复成功。当前原生请求与图片响应字段见 Google 图片生成文档。
最小文字生成请求体如下。它使用 contents[].parts 输入与 generationConfig.responseModalities 输出意图,不涉及 Vertex 的项目、地区和 IAM 配置:
{
"contents": [{
"role": "user",
"parts": [{"text": "生成一张白色背景上的蓝色陶瓷杯图片。"}]
}],
"generationConfig": {"responseModalities": ["TEXT", "IMAGE"]}
}编辑图片时,在同一条用户消息的 parts 中加入 inlineData,提供真实 MIME 类型和原图的 Base64 数据,并把文字改成具体编辑要求。例如“保留杯子形状,把杯身改成绿色”。原图与编辑要求都要保留到重试中;只把文字送给备用模型并不能完成原来的编辑任务。
可复制的 Node.js 有界重试函数
下面使用支持原生 fetch 的 Node.js,直接调用 REST,不依赖 SDK 的默认重试。示例配置是最多 4 次尝试,包括首次;总预算 60 秒;单次最多 20 秒;完全抖动从 1 秒窗口开始增长,窗口上限 8 秒。这些是演示参数,不是 Google 的容量、平均出图时间或恢复承诺。根据你自己的交付时限调整;长任务若经常超过单次时限,应改为合适的异步流程,而不是反复制造未知结果。
保存为 nano503.mjs。函数只在明确收到 HTTP 503 后再次发送;其他状态返回给对应排错分支。Retry-After 可以是秒数或 HTTP 日期,等待时间不会短于服务端提示,超过总预算就延期。HTTP 503 与 Retry-After 标准允许服务端给出提示,但不保证每次都会返回该头。
import { performance } from "node:perf_hooks";
export function retryAfterMs(value, wallNow) {
if (!value) return 0;
const s = value.trim();
if (/^\d+$/.test(s)) return Number(s) * 1000;
const date = Date.parse(s);
return Number.isFinite(date) ? Math.max(0, date - wallNow) : 0;
}
export function imageParts(body) {
const images = [];
const candidates = Array.isArray(body?.candidates) ? body.candidates : [];
for (const c of candidates) {
const parts = Array.isArray(c?.content?.parts) ? c.content.parts : [];
for (const p of parts) {
if (p?.thought) continue;
const d = p?.inlineData;
if (typeof d?.data !== "string" || !["image/png", "image/jpeg", "image/webp"].includes(d.mimeType)) continue;
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(d.data)) continue;
const bytes = Buffer.from(d.data, "base64");
if (bytes.toString("base64").replace(/=+$/, "") !== d.data.replace(/=+$/, "")) continue;
const png = bytes.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"));
const jpg = bytes.length > 4 && bytes[0] === 255 && bytes[1] === 216 &&
bytes[2] === 255 && bytes.at(-2) === 255 && bytes.at(-1) === 217;
const webp = bytes.length > 12 && bytes.toString("ascii", 0, 4) === "RIFF" &&
bytes.toString("ascii", 8, 12) === "WEBP";
if ((d.mimeType === "image/png" && png) || (d.mimeType === "image/jpeg" && jpg) ||
(d.mimeType === "image/webp" && webp)) images.push({ mime: d.mimeType, bytes });
}
}
return images;
}
export async function postOnce({ url, apiKey, payload, timeoutMs }) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const r = await fetch(url, {
method: "POST", signal: controller.signal,
headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
body: JSON.stringify(payload),
});
return { status: r.status, retryAfter: r.headers.get("retry-after"), text: await r.text() };
} finally {
clearTimeout(timer);
}
}
export async function generateBounded({ apiKey, payload, maxAttempts = 4,
budgetMs = 60000, perCallMs = 20000, send = postOnce,
now = () => performance.now(), wallNow = () => Date.now(),
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), random = Math.random }) {
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 ||
!Number.isFinite(budgetMs) || budgetMs <= 0 || !Number.isFinite(perCallMs) || perCallMs <= 0) {
throw new Error("invalid retry budget");
}
const start = now(), trace = [];
const done = (state, extra = {}) => ({ state, elapsedMs: now() - start, trace, ...extra });
const url = "https://generativelanguage.googleapis.com/v1/models/gemini-3.1-flash-image:generateContent";
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const remaining = budgetMs - (now() - start);
if (remaining <= 0) return done("deferred", { reason: "elapsed_budget" });
let r;
try {
r = await send({ url, apiKey, payload, timeoutMs: Math.min(perCallMs, remaining) });
} catch {
// 请求可能已经执行:未知传输错误不自动重放 POST。
return done("paused", { reason: "transport_result_unknown", attempt });
}
trace.push({ attempt, http: r.status });
let body;
try { body = JSON.parse(r.text); } catch { body = null; }
if (r.status >= 200 && r.status < 300) {
const images = imageParts(body);
return images.length ? done("image_received", { images }) :
done("stopped", { reason: "no_usable_image", body });
}
if (r.status !== 503) return done("stopped", { reason: "non_503", http: r.status, body });
if (attempt === maxAttempts) return done("deferred", { reason: "attempt_budget", body });
const jitter = random() * Math.min(8000, 1000 * 2 ** (attempt - 1));
const delay = Math.max(jitter, retryAfterMs(r.retryAfter, wallNow()));
if (delay >= budgetMs - (now() - start)) {
return done("deferred", { reason: "retry_after_or_elapsed_budget", waitMs: delay });
}
await sleep(delay);
}
}下面的调用函数完成输入读取、生成或编辑请求,以及图片文件保存。保存为同目录的 example.mjs;将 createOrEdit 接入已有应用,apiKey 由你现有的安全凭证管理提供。未传 inputPath 时生成新图;传入本地 JPG 路径时编辑原图。其他输入格式要同步修改 MIME 类型,不能只改文件扩展名。
import { readFile, writeFile } from "node:fs/promises";
import { generateBounded } from "./nano503.mjs";
export async function createOrEdit(apiKey, inputPath = null) {
const parts = [{ text: inputPath
? "保留杯子形状,把杯身改成绿色。"
: "生成一张白色背景上的蓝色陶瓷杯图片。" }];
if (inputPath) {
parts.push({ inlineData: {
mimeType: "image/jpeg",
data: (await readFile(inputPath)).toString("base64"),
} });
}
const result = await generateBounded({ apiKey, payload: {
contents: [{ role: "user", parts }],
generationConfig: { responseModalities: ["TEXT", "IMAGE"] },
} });
if (result.state !== "image_received") return result;
const ext = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
const paths = [];
for (const [i, image] of result.images.entries()) {
const path = `output-${i + 1}.${ext[image.mime]}`;
await writeFile(path, image.bytes);
paths.push(path);
}
return { ...result, paths };
}这份代码已经通过假传输和假时钟的离线检查,覆盖 503 后恢复、非 503 停止、次数耗尽、请求耗时挤占预算、秒数/日期形式的 Retry-After、未知超时暂停,以及无图片和错误图片数据。没有调用真实 Gemini API,也没有测量当前成功率或延迟。
别让应用、SDK、网关同时重试
选一个地方负责重试,并确认其他层不会偷偷增加尝试数。例如应用重试 4 次,每次 SDK 再尝试 5 次,就可能变成 20 次上游调用,还没有计入网关。
Google 排错文档描述 Python SDK 自动重试 4 次;而本次查看的 python-genai main 实现在 retry_options=None 时只尝试一次。源码分支不是你已安装版本的保证,因此不要凭默认值估算预算。若采用当前 google-genai、from google import genai 且由应用负责重试,可显式使用 types.HttpOptions(retry_options=types.HttpRetryOptions(attempts=1), timeout=20000),并确认安装版本支持这些字段。这里 attempts=1 表示只做首次调用;timeout=20000 的单位是毫秒,即 20 秒,字段定义见 SDK types.py。
Python 原生 SDK 异常应按 errors.APIError.code == 503 判断,code 是整数,见 APIError 定义。另一个 Interactions 错误参考使用字符串错误码,不能把其中的 service_unavailable 当作原生 generateContent SDK 的字段约定。
怎样确认真的出图,预算用完又怎样处理
上面的 image_received 只表示取到了图片候选字节,完成了 Base64、MIME 类型与文件头的初步检查。它还不是最终交付状态:用应用现有图片解码器完整解码,检查宽高,再打开文件确认内容符合生成或编辑要求。文件头正确也可能是截断文件;要求编辑原图时,还需检查所要求的修改是否实际发生。
验收顺序是:
- 遍历
candidates[].content.parts[],找到非思考部分的inlineData图片,而不是只取第一段文本。 - 确认 MIME 类型、Base64 和非空字节匹配。使用 SDK 时注意其字段可能是
inline_data,不要把 REST 与 SDK 对象混用。 - 保存并完整解码,确认尺寸和可读性;然后检查输出内容。到这里才能将业务任务标为完成。
- 没有图片时记录实际候选结束原因、阻止信息与响应摘要;不要把空输出直接返回成成功,也不要自动再跑整个 503 循环。
其他返回状态需要有明确去向:
| 函数状态 | 用户界面与后台动作 |
|---|---|
stopped | 给出与错误分支对应的处理结果;不自动重新提交相同任务 |
paused | 标为“结果待核对”,保留本地任务 ID 和日志;先查是否已有输出,再决定是否补发 |
deferred | 告知暂未完成;能接受延迟就保存到队列,否则结束本次交互 |
image_received | 进入完整解码和内容验收,通过后交付 |
队列里保存同一个业务任务 ID、原始请求的受保护引用、当前状态、累计尝试数、任务到期时间和下次可尝试时间。一个任务同时只允许一个工作进程持有执行租约;用户连续点击也应复用待处理任务,不创建一串副本。本地去重是你的业务控制,不代表 Google 会按这个 ID 对生成 POST 去重。
同步预算耗尽后,不要让后台自动获得无限新预算。后台仍需限制总尝试数和任务寿命,到期就结束或交给用户重新选择。若 Retry-After 超过当前交互预算,按提示延期;不能为了赶上 60 秒窗口,把服务端要求的 120 秒压成 8 秒后立即发送。
超时尤其要谨慎:中止客户端等待不等于撤销服务端生成。HTTP 标准对非幂等请求的自动重试要求额外依据;这里没有假设图片 POST 具备去重保证。确认结果之前盲目重放,可能产生重复图片与额外工作。
降低并发,恢复时只放一个半开探针

沿用旧版断路器示意图;实际阈值、等待时间与探针许可按下面的流程设计。
如果很多任务同时遇到 503,先暂停新任务进入执行区,降低同一入口、同一模型的活动请求数,让其余任务排队。不要给每个任务都增加重试次数,这会放大瞬时压力。并发限制应覆盖正在请求、等待退避后准备再次请求的同一任务,而不只是某一次 HTTP 调用。
持续失败时可以加断路器,但旧式“进入半开后所有请求都能执行”的写法会在恢复窗口重新制造拥堵。可按下面的执行顺序实现:
- 正常状态:在并发上限内执行,记录实际成功图片与瞬态失败。
- 失败达到你定义的阈值:进入打开状态,把新任务留在队列,记录下次检查时间。阈值和冷却时长应依据自己的日志与交付要求设置。
- 冷却结束:用锁或原子比较交换领取唯一的半开许可。其他工作进程没有许可就继续等待;分布式服务要使用共享许可,不能各实例放一个探针。
- 探针只用一个允许执行的实际业务任务,最多一次请求,不套多次重试。收到并验收图片后,再逐步增加并发;明确 503 则重新打开,未知超时则保留待核对状态并继续限制流量。
已有在途请求的迟到结果不能把新的打开状态随意改回正常:给状态转换增加代数标记,只允许持有当前半开许可的任务完成该次转换。这个控制能减少自身流量造成的冲击,但不能保证 Google 在某个固定分钟数内恢复。也不需要用定时付费生图来“刷健康状态”。
什么时候排队、换图片模型或用 Batch
选择取决于交付时限与图片任务要求。能等待的任务优先保留原模型和原始输入排队;必须尽快交付的任务,才考虑已经接入、允许使用并且满足要求的备用图片模型。
备用模型应提前核对四件事:是否接受原图进行编辑、输出尺寸或比例能否满足要求、数据处理约定是否适用、总预算是否容纳再次生成。切换后照样验收图片,不把文本解释当成替代结果。没有经过这类核对的备用路线,就明确暂停或排队,不能临时换协议、换到 Vertex IAM 流程,或承诺另一个模型一定更稳定。
Nano Banana 2 的模型文档列出支持 Batch。Gemini Batch API适合非紧急 generateContent 任务,目标完成时间为 24 小时,但这是处理目标,不是交付 SLA,也不是“没有任何 503 风险”的保证。图片任务仍可能失败、取消或过期。
已有 Batch 作业时保存并查询同一个作业名,不因暂时没有结果就反复创建整个批次。作业成功后,也要逐项读取错误和图片数据;作业成功不等于每条图片请求成功。只重新处理确认失败且仍需要交付的条目,避免重复生成已完成图片。
常见问题
Nano Banana 2 的 503 失败请求一定不收费吗?
不能这样保证。Google 计费 FAQ明确提到失败的 400 或 500 请求不计 token 费用,但仍计入配额;该说明没有明确覆盖所有 503、第三方封装退款或未知网络超时。核对你实际入口的用量、账单与请求记录。重试、备用模型的新尝试以及超时后已经完成的生成,都不能预先视为免费。
升级付费层级或轮换 API key 能解决过载吗?
付费或配额变更不能保证修复 503。若实际错误是 429,应先确认当前受限维度,再按对应规则处理;不能用“升级一定解决”代替诊断。503 分支先减压、有限退避和延期,不通过轮换 key 或项目规避限制。
应该等多久再试,有统一的低峰时间吗?
本页没有当前成功率、统一低峰时间或固定恢复时长的实测依据。有 Retry-After 时尊重提示;没有时按自己的交付预算退避,超预算就延期。安排后台任务时,可利用自己同入口、同模型、相同请求形态的日志观察趋势,不能把旧社区比例当作今天的调度承诺。
503 消失、只返回文字,算恢复了吗?
对图片任务而言还不算。确认请求仍使用图片模型和图片输出配置,检查候选部分是否有可用图片数据,再完成解码和内容验收。若只回文字,应进入无图片分支检查真实响应,而不是宣布服务已恢复或继续盲目重试。
官方文档
接入参数、价格与扣费规则以老张API开发文档为准:图片生成计费与失败处理。
说明:laozhang.ai 是我们自己的服务,其价格是平台报价,不是独立比较的结论。
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月7日。
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月7日。
- 1.Nano Banana 2 模型文档ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image
- 2.Google 的排错文档ai.google.dev/gemini-api/docs/troubleshooting
- 3.Google 图片生成文档ai.google.dev/gemini-api/docs/generate-content/image-generation
- 4.HTTP 503 与 Retry-After 标准rfc-editor.org/rfc/rfc9110.html
- 5.python-genai main 实现github.com/googleapis/python-genai/blob/main/google/genai/_api_client.py
- 6.SDK types.pygithub.com/googleapis/python-genai/blob/main/google/genai/types.py
- 7.APIError 定义github.com/googleapis/python-genai/blob/main/google/genai/errors.py
- 8.Interactions 错误参考ai.google.dev/gemini-api/docs/api-errors
- 9.Gemini Batch APIai.google.dev/gemini-api/docs/batch-api
- 10.Google 计费 FAQai.google.dev/gemini-api/docs/billing





