# Nano Banana 2 503 模型过载：有界重试、暂停与出图验收

> Nano Banana 2 返回 503 时，先确认接口和 HTTP 状态，再在次数与总耗时预算内退避重试；预算耗尽就暂停或排队，收到图片数据并完成解码才算出图成功。

- URL: https://blog.laozhang.ai/zh/posts/nano-banana-2-503-overloaded-fix
- Published: 2026-03-09
- Updated: 2026-10-07
- Author: LaoZhang AI Team (https://blog.laozhang.ai/zh/about)
- Topic: AI 图像生成
- Tags: Nano Banana 2, 503错误修复, Gemini API, API错误处理

---
Nano Banana 2 返回 `503`、`UNAVAILABLE` 或“模型过载”时，先保留实际接口、模型 ID 和完整 HTTP 响应，再做**有次数上限、总耗时上限和随机抖动的退避重试**。预算用完后暂停或排队，不让用户一直转圈。最后必须检查响应里的图片数据，并确认文件能解码、能打开；HTTP 200 或一段文字都不等于出图成功。

当前 [Nano Banana 2 模型文档](https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image)对应的图片模型是 `gemini-3.1-flash-image`。下文针对 Gemini Developer API 原生 `generateContent` 的生成和编辑流程。Gemini 应用、Vertex AI、第三方供应商的接口与错误约定需要分别确认，不能直接套用。

## 先分清：503、429、没图片，还是请求超时

![按明确 503、429、其他 HTTP 错误、200 无图和未知超时分流的排查示意。](https://blog.laozhang.ai/posts/zh/nano-banana-2-503-overloaded-fix/img/error-diagnosis-flowchart.webp)



先查看 HTTP 状态码，再读结构化错误字段。不要用 `"503" in str(error)` 或搜索“overloaded”来决定重试：错误说明里可能引用别的错误码，代理也可能返回自己的错误页。

| 实际信号 | 此时能判断什么 | 下一动作 |
|---|---|---|
| HTTP 503，收到明确错误响应 | 当前请求所经服务暂时不可用；可能是过载或维护 | 在预算内退避重试；保存响应来源 |
| HTTP 429 | 需要区分速率限制、配额耗尽及当前限制维度 | 速率限制可按提示等待；额度耗尽先停止，不进入本页的 503 循环 |
| HTTP 400、402、403、404 | 请求、支付前提、权限或资源存在其他问题 | 按完整错误修正原因，不原样循环重试 |
| HTTP 200，但没有可用图片 | 传输成功，图片任务仍未完成 | 查看候选内容、结束原因、阻止信息与解析逻辑 |
| 客户端超时、连接断开，没有确定结果 | 不知道服务是否已经完成生成 | 暂停并核对日志或已有任务结果，不自动重放 |

[Google 的排错文档](https://ai.google.dev/gemini-api/docs/troubleshooting)建议对瞬态错误采用退避、抖动和有限重试。但“200 无图”不能一律归因于安全拦截：输出配置、模型选择、候选结束原因和代码只读取文本，都可能改变判断。先看真实响应，不急着改提示词。

503 也不能单独证明“全球所有用户都受影响”。记录时间、接口域名、API 版本、模型、HTTP 状态、脱敏错误和 request ID（如果有）。第三方入口返回 503 时，要区分其网关错误与 Google 上游响应；影响范围不清楚，可先按 [Nano Banana 2 宕机判断指南](https://blog.laozhang.ai/zh/posts/is-nano-banana-2-down)核对对应入口和状态页。

## 用当前图片请求恢复生成或编辑

图片请求应保持任务本身：生成时提供文字，编辑时提供文字和原图，输出仍要求图片。不能把 `gemini-2.5-flash` 等文本模型换上来，然后把返回文字当作恢复成功。当前原生请求与图片响应字段见 [Google 图片生成文档](https://ai.google.dev/gemini-api/docs/generate-content/image-generation)。

最小文字生成请求体如下。它使用 `contents[].parts` 输入与 `generationConfig.responseModalities` 输出意图，不涉及 Vertex 的项目、地区和 IAM 配置：

```json
{
  "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 标准](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.6.4)允许服务端给出提示，但不保证每次都会返回该头。

```javascript
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 类型，不能只改文件扩展名。

```javascript
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 实现](https://github.com/googleapis/python-genai/blob/main/google/genai/_api_client.py)在 `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](https://github.com/googleapis/python-genai/blob/main/google/genai/types.py)。

Python 原生 SDK 异常应按 `errors.APIError.code == 503` 判断，`code` 是整数，见 [APIError 定义](https://github.com/googleapis/python-genai/blob/main/google/genai/errors.py)。另一个 [Interactions 错误参考](https://ai.google.dev/gemini-api/docs/api-errors)使用字符串错误码，不能把其中的 `service_unavailable` 当作原生 `generateContent` SDK 的字段约定。

## 怎样确认真的出图，预算用完又怎样处理

上面的 `image_received` 只表示取到了图片候选字节，完成了 Base64、MIME 类型与文件头的初步检查。它还不是最终交付状态：**用应用现有图片解码器完整解码，检查宽高，再打开文件确认内容符合生成或编辑要求**。文件头正确也可能是截断文件；要求编辑原图时，还需检查所要求的修改是否实际发生。

验收顺序是：

1. 遍历 `candidates[].content.parts[]`，找到非思考部分的 `inlineData` 图片，而不是只取第一段文本。
2. 确认 MIME 类型、Base64 和非空字节匹配。使用 SDK 时注意其字段可能是 `inline_data`，不要把 REST 与 SDK 对象混用。
3. 保存并完整解码，确认尺寸和可读性；然后检查输出内容。到这里才能将业务任务标为完成。
4. 没有图片时记录实际候选结束原因、阻止信息与响应摘要；不要把空输出直接返回成成功，也不要自动再跑整个 503 循环。

其他返回状态需要有明确去向：

| 函数状态 | 用户界面与后台动作 |
|---|---|
| `stopped` | 给出与错误分支对应的处理结果；不自动重新提交相同任务 |
| `paused` | 标为“结果待核对”，保留本地任务 ID 和日志；先查是否已有输出，再决定是否补发 |
| `deferred` | 告知暂未完成；能接受延迟就保存到队列，否则结束本次交互 |
| `image_received` | 进入完整解码和内容验收，通过后交付 |

队列里保存同一个业务任务 ID、原始请求的受保护引用、当前状态、累计尝试数、任务到期时间和下次可尝试时间。一个任务同时只允许一个工作进程持有执行租约；用户连续点击也应复用待处理任务，不创建一串副本。本地去重是你的业务控制，不代表 Google 会按这个 ID 对生成 POST 去重。

同步预算耗尽后，不要让后台自动获得无限新预算。后台仍需限制总尝试数和任务寿命，到期就结束或交给用户重新选择。若 `Retry-After` 超过当前交互预算，按提示延期；不能为了赶上 60 秒窗口，把服务端要求的 120 秒压成 8 秒后立即发送。

超时尤其要谨慎：中止客户端等待不等于撤销服务端生成。HTTP 标准对[非幂等请求的自动重试](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2)要求额外依据；这里没有假设图片 POST 具备去重保证。确认结果之前盲目重放，可能产生重复图片与额外工作。

## 降低并发，恢复时只放一个半开探针

![断路器关闭、打开与半开状态之间的转换示意图](https://blog.laozhang.ai/posts/zh/nano-banana-2-503-overloaded-fix/img/circuit-breaker-states.webp)

沿用旧版断路器示意图；实际阈值、等待时间与探针许可按下面的流程设计。

如果很多任务同时遇到 503，先暂停新任务进入执行区，降低同一入口、同一模型的活动请求数，让其余任务排队。不要给每个任务都增加重试次数，这会放大瞬时压力。并发限制应覆盖**正在请求、等待退避后准备再次请求**的同一任务，而不只是某一次 HTTP 调用。

持续失败时可以加断路器，但旧式“进入半开后所有请求都能执行”的写法会在恢复窗口重新制造拥堵。可按下面的执行顺序实现：

- 正常状态：在并发上限内执行，记录实际成功图片与瞬态失败。
- 失败达到你定义的阈值：进入打开状态，把新任务留在队列，记录下次检查时间。阈值和冷却时长应依据自己的日志与交付要求设置。
- 冷却结束：**用锁或原子比较交换领取唯一的半开许可**。其他工作进程没有许可就继续等待；分布式服务要使用共享许可，不能各实例放一个探针。
- 探针只用一个允许执行的实际业务任务，最多一次请求，不套多次重试。收到并验收图片后，再逐步增加并发；明确 503 则重新打开，未知超时则保留待核对状态并继续限制流量。

已有在途请求的迟到结果不能把新的打开状态随意改回正常：给状态转换增加代数标记，只允许持有当前半开许可的任务完成该次转换。这个控制能减少自身流量造成的冲击，但不能保证 Google 在某个固定分钟数内恢复。也不需要用定时付费生图来“刷健康状态”。

## 什么时候排队、换图片模型或用 Batch

选择取决于交付时限与图片任务要求。能等待的任务优先保留原模型和原始输入排队；必须尽快交付的任务，才考虑已经接入、允许使用并且满足要求的备用图片模型。

备用模型应提前核对四件事：是否接受原图进行编辑、输出尺寸或比例能否满足要求、数据处理约定是否适用、总预算是否容纳再次生成。切换后照样验收图片，不把文本解释当成替代结果。没有经过这类核对的备用路线，就明确暂停或排队，不能临时换协议、换到 Vertex IAM 流程，或承诺另一个模型一定更稳定。

Nano Banana 2 的模型文档列出支持 Batch。[Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api)适合非紧急 `generateContent` 任务，目标完成时间为 24 小时，但这是处理目标，不是交付 SLA，也不是“没有任何 503 风险”的保证。图片任务仍可能失败、取消或过期。

已有 Batch 作业时保存并查询同一个作业名，不因暂时没有结果就反复创建整个批次。作业成功后，也要逐项读取错误和图片数据；作业成功不等于每条图片请求成功。只重新处理确认失败且仍需要交付的条目，避免重复生成已完成图片。

## 常见问题

### Nano Banana 2 的 503 失败请求一定不收费吗？

不能这样保证。[Google 计费 FAQ](https://ai.google.dev/gemini-api/docs/billing)明确提到失败的 400 或 500 请求不计 token 费用，但仍计入配额；该说明没有明确覆盖所有 503、第三方封装退款或未知网络超时。核对你实际入口的用量、账单与请求记录。重试、备用模型的新尝试以及超时后已经完成的生成，都不能预先视为免费。

### 升级付费层级或轮换 API key 能解决过载吗？

付费或配额变更不能保证修复 503。若实际错误是 429，应先确认当前受限维度，再按对应规则处理；不能用“升级一定解决”代替诊断。503 分支先减压、有限退避和延期，不通过轮换 key 或项目规避限制。

### 应该等多久再试，有统一的低峰时间吗？

本页没有当前成功率、统一低峰时间或固定恢复时长的实测依据。有 `Retry-After` 时尊重提示；没有时按自己的交付预算退避，超预算就延期。安排后台任务时，可利用自己同入口、同模型、相同请求形态的日志观察趋势，不能把旧社区比例当作今天的调度承诺。

### 503 消失、只返回文字，算恢复了吗？

对图片任务而言还不算。确认请求仍使用图片模型和图片输出配置，检查候选部分是否有可用图片数据，再完成解码和内容验收。若只回文字，应进入无图片分支检查真实响应，而不是宣布服务已恢复或继续盲目重试。

## 官方文档

接入参数、价格与扣费规则以老张API开发文档为准：[图片生成计费与失败处理](https://docs.laozhang.ai/api-capabilities/image-billing-and-failures)。

*说明：[laozhang.ai 是我们自己的服务](https://blog.laozhang.ai/zh/about#laozhang)，其价格是平台报价，不是独立比较的结论。*

## 参考来源

本文引用的外部页面，按正文出现顺序排列。最后更新于 2026-10-07。

- [Nano Banana 2 模型文档](https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image) (ai.google.dev)
- [Google 的排错文档](https://ai.google.dev/gemini-api/docs/troubleshooting) (ai.google.dev)
- [Google 图片生成文档](https://ai.google.dev/gemini-api/docs/generate-content/image-generation) (ai.google.dev)
- [HTTP 503 与 Retry-After 标准](https://www.rfc-editor.org/rfc/rfc9110.html) (rfc-editor.org)
- [python-genai main 实现](https://github.com/googleapis/python-genai/blob/main/google/genai/_api_client.py) (github.com)
- [SDK types.py](https://github.com/googleapis/python-genai/blob/main/google/genai/types.py) (github.com)
- [APIError 定义](https://github.com/googleapis/python-genai/blob/main/google/genai/errors.py) (github.com)
- [Interactions 错误参考](https://ai.google.dev/gemini-api/docs/api-errors) (ai.google.dev)
- [Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api) (ai.google.dev)
- [Google 计费 FAQ](https://ai.google.dev/gemini-api/docs/billing) (ai.google.dev)
