Перейти к основному содержанию

Nano Banana Pro: ошибка RESOURCE_EXHAUSTED и восстановление генерации

При RESOURCE_EXHAUSTED остановите повторы для нулевой или исчерпанной суточной квоты. Для временного отказа соблюдайте задержку сервера, ограничивайте время и число попыток, а при продолжении очереди пропускайте уже сохранённые изображения.

LaoZhang AI TeamОпубликованоОбновлено 9 мин чтения
Содержание
Ошибка RESOURCE_EXHAUSTED: остановка, отложенный повтор и сохранённые задания изображения

Если Nano Banana Pro отвечает 429 RESOURCE_EXHAUSTED, сначала проверьте, какой лимит отклонил именно запрос изображения. При нулевом допустимом лимите или исчерпанной суточной квоте остановите быстрые повторы. При временном ограничении частоты уменьшите параллелизм и повторяйте с растущей паузой, соблюдая минимальное время ожидания сервера. Уже сохранённые результаты оставьте в очереди как завершённые: восстановление должно продолжать оставшиеся задания, а не заново генерировать всё.

Ни одна задержка не гарантирует доступ к модели. Но можно сделать поведение приложения предсказуемым: не повторять заведомо бесполезные запросы, не отправлять их раньше разрешённого времени и не терять изображения при частичном сбое. Ниже есть два самостоятельных примера для Gemini Developer API, REST v1beta, метода generateContent и модели gemini-3-pro-image. Они включают запрос, разбор ответа, сохранение файлов и продолжение работы. Поведение проверено локально на искусственных ответах; платные вызовы модели не выполнялись.

Найдите причину, которую действительно подтверждает ответ

Сохраните HTTP-код, тело ошибки, Retry-After, время отказа и точный ID модели. Добавьте конечную точку и версию клиента, но не API-ключ. Сравнивать ошибку с графиком использования нужно для того же проекта, модели и интервала.

В Developer API ошибка может выглядеть так:

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

Этот текст подтверждает отказ, но не называет конкретную квоту. Если есть google.rpc.QuotaFailure, изучите violations: quotaMetric, quotaId, quotaDimensions, description и присутствующее quotaValue. Последнее означает применённый предел квоты, а не расход или оставшийся запас. Отсутствующий quotaValue нельзя подставлять как ноль. Не все ответы содержат все эти поля. Определения Google RPC.

Что подтвержденоЧто делать с заданием изображения
Применённый лимит явно равен нулю, например limit: 0Остановить повторы и проверить доступ модели в проекте фактического ключа
Указана исчерпанная дневная квотаСохранить задание до сброса или изменения доступной квоты; не запускать минутный цикл
Указан ненулевой минутный лимитСнизить параллелизм, распределить запросы во времени и ограниченно повторить
429 без уточняющих полейПричина остаётся неизвестной; сопоставить с действующими квотами и допустить только ограниченные повторы
503 UNAVAILABLEРассматривать как возможный временный отказ; ограничить повторы, не назначать гарантированное время восстановления
400, 402, 403 или неверная модельИсправить запрос, оплату или доступ; автоматическая пауза сама по себе задачу не решит
Таймаут, разрыв соединения или падение процесса во время POSTСохранить неопределённый исход; не отправлять тот же платный запрос вслепую
200, но нет декодируемого изображенияПроверить promptFeedback, finishReason и части ответа; не считать генерацию завершённой

В Developer API RPM, входные TPM и RPD проверяются отдельно, а для изображений может действовать IPM. Квоты принадлежат проекту, поэтому несколько ключей одного проекта не создают независимые лимиты. RPD сбрасывается в полночь по тихоокеанскому времени; это не фиксированное местное время для всех читателей. Проверяйте действующие значения в AI Studio, а не по старой таблице квот текстовых моделей. Правила лимитов Gemini API.

Выбор действий при нулевой, суточной и временной квоте после ошибки 429

На 5 октября 2026 года официальная страница цен отмечает Free Tier для gemini-3-pro-image как недоступный. Платный статус тоже не означает отсутствие ограничений: кроме модельных квот, при соответствующих условиях аккаунта применяются ограничения расходов в скользящем десятиминутном окне. Если оплаченный проект получает free_tier_requests limit: 0, сначала выполните проверку ключа, проекта и оплаты при нулевом Free Tier. Это отдельная ветка диагностики, а не повод увеличивать число повторов. Условия Pro Image, ограничения расходов.

Если запрос идёт через Vertex AI

Не переносите на него таблицу уровней Developer API. В актуальном руководстве Google Cloud по 429 отказ Pay-as-you-go может означать отсутствие доступной выделенной ёмкости; Provisioned Throughput имеет собственные правила и условия превышения купленной пропускной способности. Полезные меры — сглаживание всплесков и постепенное наращивание нагрузки. Запрос увеличения квоты, глобальная конечная точка или резервирование мощности применимы только при подходящей модели, сервисе и условиях размещения данных. Один код 429 не доказывает, что нужно покупать резерв или менять регион.

Сколько ждать перед повтором: 2 секунды, 8 секунд или дольше

Ориентируйтесь на большую из задержек: собственную паузу с ростом и случайным разбросом, Retry-After и google.rpc.RetryInfo.retryDelay. Например, приложение выбрало 2 секунды, заголовок указал 5 секунд, а RetryInfo — 8s: повтор допустим не раньше чем через 8 секунд после получения ошибки. Случайный разброс добавляйте к локальной паузе; он не должен уменьшать серверный минимум.

Retry-After бывает числом секунд или HTTP-датой. RetryInfo требует выждать как минимум указанную длительность с момента получения ошибки. Эти значения задают ожидание, но не обещают, что следующий запрос пройдёт. RFC 9110 о Retry-After, RetryInfo.

Если сервер просит ждать 200 секунд, а осталось 20 секунд общего бюджета, отложите задание. Нельзя сократить 200 до 20 и отправить запрос раньше. Ограничение максимальной локальной паузы относится к вашему расписанию, а не к серверному минимуму.

Отдельно ограничивайте время каждого запроса и всего прохода очереди. В примерах ниже один проход имеет бюджет 120 секунд для запросов и ожидания, один запрос — до 30 секунд, одно задание — не больше четырёх отправок с учётом сохранённых попыток. Перед каждой отправкой таймаут уменьшается до остатка общего бюджета. Это выбранные параметры примера, а не рекомендованная Google квота или гарантия скорости Nano Banana Pro.

Подготовьте задания и каталог результатов

Создайте jobs.json с устойчивыми уникальными ID. Не меняйте промпт под старым ID: если нужна новая генерация, создайте новый ID.

json
[
  {"id": "hero", "prompt": "Создай студийное изображение красного чайника на белом фоне"},
  {"id": "detail", "prompt": "Создай крупный план ручки красного чайника на белом фоне"}
]

Выберите одну реализацию и отдельный каталог: Python и Node.js используют собственный формат контрольного отпечатка, поэтому их журналы нельзя смешивать. Один каталог должен обслуживать один процесс. Контрольный отпечаток связывает ID с моделью, параметрами и промптом; для каждого подтверждённого файла хранится SHA-256.

Примеры используют contents[].parts, generationConfig.imageConfig.imageSize: "1K" и candidates[].content.parts[].inlineData. В inlineData нужны MIME и base64-данные изображения. Поля image_resolution и imageResolution: "1024x1024" из старых примеров не соответствуют этой конфигурации. Pro Image поддерживает 1K, 2K и 4K; общее значение 512 в типе ImageConfig не доказывает его поддержку этой моделью. Справочник GenerateContent, возможности моделей изображений.

Текущее руководство изображений также показывает Interactions API: у него input, response_format и другая схема результата. Свойство SDK output_image не является полем ответа GenerateContent. Для миграции нужно заменить весь соответствующий запрос и обработчик ответа, а не только URL. Два примера ниже последовательно используют GenerateContent REST.

Python: запрос, ограниченные повторы и сохранение изображений

Нужны Python 3.11 или новее, httpx и Pillow. Сохраните код как recover.py. Установите зависимости в своём окружении:

bash
python -m pip install httpx Pillow

Задайте GEMINI_API_KEY через используемое вами хранилище секретов или окружение процесса. Команда ниже выполняет настоящие платные запросы, когда вы запускаете её со своим ключом; локальная проверка статьи выполнялась без этой команды и без ключа.

bash
python recover.py jobs.json out-python
python
import asyncio, base64, hashlib, io, json, os, random, re, sys, time
from email.utils import parsedate_to_datetime
from pathlib import Path
import httpx
from PIL import Image

MODEL = "gemini-3-pro-image"
URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent"
CONFIG = {"responseModalities": ["TEXT", "IMAGE"],
          "imageConfig": {"aspectRatio": "16:9", "imageSize": "1K"}}

def save_json(path, data):
    temp = path.with_suffix(".tmp")
    temp.write_text(json.dumps(data, ensure_ascii=False, indent=2))
    temp.replace(path)

def decision(status, body, headers, wall):
    err = body.get("error", {})
    details = err.get("details", [])
    violations = [v for d in details
                  if d.get("@type", "").endswith("QuotaFailure")
                  for v in d.get("violations", [])]
    zero = any("quotaValue" in v and str(v["quotaValue"]) == "0"
               for v in violations)
    text = " ".join([str(err.get("message", ""))] +
                    [json.dumps(v) for v in violations]).lower()
    daily = bool(re.search(r"per.?day|daily|requestsperday|суточ", text))
    zero = zero or bool(re.search(r"\blimit\s*[:=]\s*0\b", text))
    if status == 429 and (zero or daily):
        return "stop", 0
    if status not in (429, 503):
        return "stop", 0
    floors = [0.0]
    value = headers.get("retry-after", "").strip()
    if re.fullmatch(r"\d+", value):
        floors.append(float(value))
    elif value:
        try:
            floors.append(max(0, parsedate_to_datetime(value).timestamp() - wall))
        except (ValueError, TypeError, OverflowError):
            pass
    for d in details:
        if d.get("@type", "").endswith("RetryInfo"):
            m = re.fullmatch(r"(\d+(?:\.\d+)?)s", d.get("retryDelay", ""))
            if m:
                floors.append(float(m[1]))
    return "retry", max(floors)

def images(body):
    found = []
    for c in body.get("candidates", []):
        for p in c.get("content", {}).get("parts", []):
            b = p.get("inlineData", {})
            mime = b.get("mimeType")
            if mime not in ("image/png", "image/jpeg", "image/webp"):
                continue
            raw = base64.b64decode(b.get("data", ""), validate=True)
            with Image.open(io.BytesIO(raw)) as image:
                image.verify()
                kind = image.format
            expected = {"image/png": "PNG", "image/jpeg": "JPEG", "image/webp": "WEBP"}
            if kind != expected[mime]:
                raise ValueError("MIME не совпадает с файлом")
            found.append((mime.split("/")[1], raw))
    if not found:
        raise ValueError("Нет подтверждённого изображения; проверьте promptFeedback/finishReason")
    return found

async def post(prompt, timeout):
    key = os.environ["GEMINI_API_KEY"]
    async with httpx.AsyncClient(timeout=timeout, follow_redirects=False) as client:
        response = await asyncio.wait_for(client.post(
            URL, headers={"x-goog-api-key": key},
            json={"contents": [{"parts": [{"text": prompt}]}],
                  "generationConfig": CONFIG}), timeout)
        return response.status_code, dict(response.headers), response.json()

async def run(jobs, directory, send=post, clock=time.monotonic,
              wall=time.time, sleep=asyncio.sleep, rand=random.random, budget=120):
    directory = Path(directory)
    directory.mkdir(parents=True, exist_ok=True)
    path = directory / "state.json"
    state = json.loads(path.read_text()) if path.exists() else {}
    deadline = clock() + budget
    ids = [j["id"] for j in jobs]
    if len(ids) != len(set(ids)) or any(not re.fullmatch(r"[A-Za-z0-9_-]+", x) for x in ids):
        raise ValueError("Нужны уникальные безопасные id")
    for job in jobs:
        fingerprint = hashlib.sha256(json.dumps(
            [MODEL, CONFIG, job["prompt"]], sort_keys=True).encode()).hexdigest()
        record = state.setdefault(job["id"], {"fingerprint": fingerprint, "attempts": 0})
        if record["fingerprint"] != fingerprint:
            raise ValueError("Изменённое задание требует нового id")
        if record.get("status") == "done":
            for f in record["files"]:
                raw = (directory / f["name"]).read_bytes()
                if hashlib.sha256(raw).hexdigest() != f["sha256"]:
                    raise ValueError("Подтверждённый файл изменён")
            continue
        if record.get("status") in ("inflight", "unknown", "stop", "review"):
            continue
        while record["attempts"] < 4:
            remaining = deadline - clock()
            wait = max(0, record.get("notBefore", 0) - wall())
            if wait + 1 >= remaining:
                record["status"] = "deferred"
                save_json(path, state)
                return state
            if wait:
                await sleep(wait)
            timeout = min(30, deadline - clock())
            if timeout < 1:
                return state
            record.update(status="inflight", attempts=record["attempts"] + 1)
            save_json(path, state)  # Запись ДО отправки
            try:
                status, headers, body = await send(job["prompt"], timeout)
            except Exception:
                record["status"] = "unknown"
                save_json(path, state)
                return state  # Исход POST неизвестен
            if status == 200:
                try:
                    output = images(body)
                    files = []
                    for i, (ext, raw) in enumerate(output):
                        name = f"{job['id']}-{i}.{ext}"
                        target = directory / name
                        temp = target.with_suffix(".tmp")
                        temp.write_bytes(raw)
                        temp.replace(target)
                        files.append({"name": name, "sha256": hashlib.sha256(raw).hexdigest()})
                    record.update(status="done", files=files)
                except Exception:
                    record["status"] = "review"
                save_json(path, state)
                if record["status"] != "done":
                    return state
                break
            action, floor = decision(status, body, headers, wall())
            record.update(status="stop" if action == "stop" else "deferred", http=status)
            if action == "retry":
                delay = max(floor, min(16, 2 ** record["attempts"]) + rand())
                record["notBefore"] = wall() + delay
            save_json(path, state)
            if action == "stop" or record["attempts"] == 4:
                return state
    return state

if __name__ == "__main__":
    jobs = json.loads(Path(sys.argv[1]).read_text())
    result = asyncio.run(run(jobs, sys.argv[2]))
    print(json.dumps(result, ensure_ascii=False, indent=2))

httpx здесь не добавляет собственный цикл повторов. asyncio.wait_for ограничивает длительность сетевой операции, а журнал записывается до POST. Если операция выбрасывает исключение, статус становится unknown, и программа заканчивает проход. При успешном HTTP-ответе Pillow проверяет данные изображения перед сохранением; текстовый ответ не помечается как готовое изображение.

Node.js: тот же результат через fetch и AbortController

Нужны Node.js 22 или новее и sharp для декодирования полученных изображений. Сохраните код как recover.mjs рядом с jobs.json и установите зависимость:

bash
npm install sharp

После настройки собственного GEMINI_API_KEY запуск выглядит так:

bash
node recover.mjs jobs.json out-node

Это также платный вызов при реальном запуске. AbortController действует до окончания чтения JSON, sharp декодирует пиксели, а состояние done появляется только после записи файлов. Внешнюю библиотеку повторов поверх этого цикла добавлять не нужно.

javascript
import fs from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import sharp from 'sharp';

const model = 'gemini-3-pro-image';
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
const config = { responseModalities: ['TEXT', 'IMAGE'],
  imageConfig: { aspectRatio: '16:9', imageSize: '1K' } };
const hash = data => createHash('sha256').update(data).digest('hex');
const pause = seconds => new Promise(resolve => setTimeout(resolve, seconds * 1000));
async function save(file, state) {
  await fs.writeFile(file + '.tmp', JSON.stringify(state, null, 2));
  await fs.rename(file + '.tmp', file);
}
export function decision(status, body, headers, now) {
  const error = body.error ?? {}, details = error.details ?? [];
  const violations = details.filter(d => d['@type']?.endsWith('QuotaFailure'))
    .flatMap(d => d.violations ?? []);
  const text = `${error.message ?? ''} ${JSON.stringify(violations)}`;
  const zero = violations.some(v => Object.hasOwn(v, 'quotaValue') && String(v.quotaValue) === '0')
    || /\blimit\s*[:=]\s*0\b/i.test(text);
  if (status === 429 && (zero || /per.?day|daily|requestsperday|суточ/i.test(text)))
    return { action: 'stop', floor: 0 };
  if (![429, 503].includes(status)) return { action: 'stop', floor: 0 };
  const floors = [0], value = (headers['retry-after'] ?? '').trim();
  if (/^\d+$/.test(value)) floors.push(Number(value));
  else if (value && Number.isFinite(Date.parse(value)))
    floors.push(Math.max(0, Date.parse(value) / 1000 - now));
  for (const d of details) {
    if (d['@type']?.endsWith('RetryInfo')) {
      const match = /^(\d+(?:\.\d+)?)s$/.exec(d.retryDelay ?? '');
      if (match) floors.push(Number(match[1]));
    }
  }
  return { action: 'retry', floor: Math.max(...floors) };
}
export async function images(body) {
  const found = [];
  for (const c of body.candidates ?? []) for (const p of c.content?.parts ?? []) {
    const b = p.inlineData ?? {}, formats = { 'image/png': 'png', 'image/jpeg': 'jpeg', 'image/webp': 'webp' };
    if (!formats[b.mimeType]) continue;
    if (typeof b.data !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(b.data))
      throw new Error('Неверный base64');
    const raw = Buffer.from(b.data, 'base64');
    const decoder = sharp(raw), metadata = await decoder.metadata();
    await decoder.raw().toBuffer(); // Декодировать реальные пиксели
    if (metadata.format !== formats[b.mimeType]) throw new Error('MIME не совпадает');
    found.push({ ext: formats[b.mimeType], raw });
  }
  if (!found.length) throw new Error('Нет изображения; проверьте promptFeedback/finishReason');
  return found;
}
async function post(prompt, timeout) {
  if (!process.env.GEMINI_API_KEY) throw new Error('Не задан GEMINI_API_KEY');
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout * 1000);
  try {
    const response = await fetch(url, { method: 'POST', redirect: 'error',
      signal: controller.signal,
      headers: { 'Content-Type': 'application/json', 'x-goog-api-key': process.env.GEMINI_API_KEY },
      body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: config }) });
    return [response.status, Object.fromEntries(response.headers), await response.json()];
  } finally { clearTimeout(timer); }
}
export async function run(jobs, directory, opts = {}) {
  const send = opts.send ?? post, clock = opts.clock ?? (() => performance.now() / 1000);
  const wall = opts.wall ?? (() => Date.now() / 1000), sleep = opts.sleep ?? pause;
  const rand = opts.rand ?? Math.random, deadline = clock() + (opts.budget ?? 120);
  await fs.mkdir(directory, { recursive: true });
  const file = path.join(directory, 'state.json');
  let state = {};
  try { state = JSON.parse(await fs.readFile(file, 'utf8')); }
  catch (error) { if (error.code !== 'ENOENT') throw error; }
  const ids = jobs.map(j => j.id);
  if (new Set(ids).size !== ids.length || ids.some(id => !/^[A-Za-z0-9_-]+$/.test(id)))
    throw new Error('Нужны уникальные безопасные id');
  for (const job of jobs) {
    const fingerprint = hash(JSON.stringify([model, config, job.prompt]));
    const record = state[job.id] ??= { fingerprint, attempts: 0 };
    if (record.fingerprint !== fingerprint) throw new Error('Изменённому заданию нужен новый id');
    if (record.status === 'done') {
      for (const f of record.files)
        if (hash(await fs.readFile(path.join(directory, f.name))) !== f.sha256)
          throw new Error('Подтверждённый файл изменён');
      continue;
    }
    if (['inflight', 'unknown', 'stop', 'review'].includes(record.status)) continue;
    while (record.attempts < 4) {
      const wait = Math.max(0, (record.notBefore ?? 0) - wall());
      if (wait + 1 >= deadline - clock()) {
        record.status = 'deferred'; await save(file, state); return state;
      }
      if (wait) await sleep(wait);
      const timeout = Math.min(30, deadline - clock());
      if (timeout < 1) return state;
      record.status = 'inflight'; record.attempts++; await save(file, state);
      let status, headers, body;
      try { [status, headers, body] = await send(job.prompt, timeout); }
      catch { record.status = 'unknown'; await save(file, state); return state; }
      if (status === 200) {
        try {
          const output = await images(body), files = [];
          for (const [i, { ext, raw }] of output.entries()) {
            const name = `${job.id}-${i}.${ext}`, target = path.join(directory, name);
            await fs.writeFile(target + '.tmp', raw); await fs.rename(target + '.tmp', target);
            files.push({ name, sha256: hash(raw) });
          }
          record.status = 'done'; record.files = files;
        } catch { record.status = 'review'; }
        await save(file, state);
        if (record.status !== 'done') return state;
        break;
      }
      const { action, floor } = decision(status, body, headers, wall());
      record.status = action === 'stop' ? 'stop' : 'deferred'; record.http = status;
      if (action === 'retry') record.notBefore = wall() + Math.max(floor, Math.min(16, 2 ** record.attempts) + rand());
      await save(file, state);
      if (action === 'stop' || record.attempts === 4) return state;
    }
  }
  return state;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const jobs = JSON.parse(await fs.readFile(process.argv[2], 'utf8'));
  console.log(JSON.stringify(await run(jobs, process.argv[3]), null, 2));
}

Как продолжить очередь после частичного сбоя

Запустите выбранную программу с тем же jobs.json и тем же каталогом результатов. Она прочитает state.json, проверит отпечатки завершённых файлов и пропустит задания со статусом done. Если первое изображение сохранено, а второе получило временный отказ, первое повторно не отправляется; оставшееся задание учитывает сохранённые попытки и время notBefore.

Продолжение очереди: готовые изображения, отложенные задачи и неопределённый исход POST

Состояние в журналеЧто означает и как продолжать
doneИзображения проверены и записаны; программа проверяет их SHA-256 и пропускает генерацию
deferredПолучен явный временный отказ; повтор возможен после notBefore, если остались попытки и время
stopПолучена ошибка доступа, оплаты, нулевая или дневная квота; сначала устраните подтверждённую причину
unknownИсключение во время вызова; сервер мог обработать POST, но клиент не получил подтверждения
inflight при следующем запускеПроцесс прервался после записи намерения отправить запрос; исход тоже требует ручного разбора
reviewHTTP 200 не дал проверяемых изображений либо возникла проблема записи; исследуйте ответ или файлы перед новой генерацией

unknown, оставшийся inflight, stop и review автоматически не отправляются повторно. Для неопределённого исхода проверьте доступные журналу приложения данные и учёт использования, восстановите результат из сохранённого ответа, если он есть. Эти примеры не предоставляют серверную операцию поиска результата по локальному ID. Если подтвердить результат нельзя, решение о новой платной отправке принимает владелец задачи; новый ID явно обозначит эту новую операцию.

Для stop после подтверждённого исправления причины остановите процесс и измените только status этого задания в state.json на deferred. Сохраните fingerprint, attempts и записи остальных заданий, затем запустите программу снова. Это ручное разрешение новой отправки, а не автоматическое исправление квоты. Не удаляйте весь state.json, чтобы «начать сначала»: так исчезнут подтверждения уже готовых изображений. После четырёх отправок программа не продолжает это задание автоматически даже при новом запуске. Новая серия попыток требует осознанного решения и собственного бюджета.

Журнал защищает подтверждённые локальные результаты, но не обеспечивает exactly once на сервере. Между завершением POST, записью файла и записью done возможен сбой. Поэтому программа оставляет неопределённые задания для разбора. Файловые переименования дают атомарную замену в обычной локальной файловой системе, однако пример не включает блокировки нескольких процессов, транзакционную базу данных или гарантии сохранности при отказе диска. Для сервиса с несколькими работниками это нужно добавить до внедрения.

Что действительно проверено офлайн

Для каждой реализации выполнено 15 поведенческих проверок на искусственных ответах и локальном PNG. Проверены остановка при явно нулевой и дневной квоте и при 402; приоритет RetryInfo над более коротким Retry-After; оба формата заголовка; откладывание слишком длинного ожидания; четыре отправки при продолжающемся 503; ограниченное восстановление при отсутствующем quotaValue; сохранение изображения и отказ при текстовом или повреждённом результате; отсутствие повторной отправки после таймаута и незавершённого inflight; пропуск подтверждённого задания при продолжении очереди; сокращение таймаута до остатка бюджета.

Это проверка локальной логики, не измерение доступности Google, скорости модели, тарифа аккаунта или стоимости неудачных запросов. Перед подключением к приложению адаптируйте хранение, наблюдаемость и бюджет под него. При восстановлении нагрузки начинайте с малого параллелизма: короткая последовательная очередь лучше нового одновременного всплеска.

Когда выбирать другую модель

Смена модели имеет смысл, если её результат подходит задаче и у проекта действительно есть доступная квота. Она не исправляет предел исходной модели. Сохраните выбранную модель вместе с заданием и согласуйте изменение качества, разрешения, цены и формата ответа до генерации.

По документации на 5 октября 2026 года для новых задач можно оценить Gemini 3.1 Flash Image или Flash Lite Image. Старый автоматический переход на gemini-2.5-flash-image использовать не стоит: страница цен указывает отключение 2 октября 2026 года. А текущий image guide сообщает, что Imagen недоступен через Gemini API. Это документированные состояния, а не результаты платных контрольных вызовов. Выбор модели, график отключения 2.5 Flash Image.

Если меняете Developer API на Vertex AI или сторонний шлюз, отдельно проверьте аутентификацию, модель, протокол, хранение данных и стоимость. Одинаковое название модели не означает одинаковые квоты и доступность. Без такого сравнения обещание «переключиться и навсегда забыть о 429» не обосновано.

Часто задаваемые вопросы

Почему платный Nano Banana Pro всё равно получает RESOURCE_EXHAUSTED?

Платный проект сохраняет модельные ограничения частоты и квоты; при соответствующих условиях действуют и ограничения расходов. 429 сам по себе не показывает, какое ограничение сработало. Сопоставьте детали отказа с квотами конкретного проекта. Если в ответе именно нулевой Free Tier, выполните отдельную проверку проекта и оплаты вместо обычного минутного повтора. Механика квот.

Нужно ли ждать ровно минуту при каждом 429?

Нет. Для временного отказа учитывайте серверный минимум и растущую локальную задержку. При дневном ограничении нужен дневной сброс, а при нулевом допустимом лимите — исправление доступа. Минутная пауза не является универсальным способом восстановления. Правила лимитов, RetryInfo.

Достаточно ли получить HTTP 200 для подтверждения генерации?

Нет. Для GenerateContent нужны части inlineData с подходящим MIME и декодируемыми данными изображения. Ответ может содержать текст или сведения о блокировке. Сохраняйте результат как завершённый только после проверки и записи файлов. Схема ответа GenerateContent.

Можно ли повторить запрос после таймаута без риска повторной оплаты?

По одному таймауту это определить нельзя: клиент не знает, успел ли сервер завершить обработку. Локальный ID задания сам по себе не создаёт серверную идемпотентность. Сохраните неопределённое состояние, проверьте доступные результаты и принимайте решение о новой отправке отдельно. Эти примеры не гарантируют отсутствие повторной генерации или повторных затрат.

Источники8

Внешние страницы, на которые ссылается это руководство, в порядке упоминания. Последнее обновление: 5 окт. 2026 г..

  1. 1.Определения Google RPCgithub.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
  2. 2.AI Studioaistudio.google.com/usage
  3. 3.Правила лимитов Gemini APIai.google.dev/gemini-api/docs/rate-limits
  4. 4.Условия Pro Imageai.google.dev/gemini-api/docs/pricing
  5. 5.руководстве Google Cloud по 429docs.cloud.google.com/gemini-enterprise-agent-platform/models/deploy/error-code-429
  6. 6.RFC 9110 о Retry-Afterrfc-editor.org/rfc/rfc9110
  7. 7.Справочник GenerateContentai.google.dev/api/generate-content
  8. 8.возможности моделей изображенийai.google.dev/gemini-api/docs/image-generation
Разбор ошибок Gemini API: 429 RESOURCE_EXHAUSTED, 400 и серверные ошибки 5xx
Руководства по API

Устранение ошибок Gemini API: 429 RESOURCE_EXHAUSTED, 400, 500

429 RESOURCE_EXHAUSTED в Gemini API бывает разным: поминутный лимит проходит за секунды, месячный — 1-го числа. Как понять по ответу, повторять ли запрос.

15 мин
Проверка источника и полей ошибки Pro: отдельные действия для подтверждённого 503, 504, неизвестного сетевого исхода и нового кода
Руководства по API

Ошибка 503 Nano Banana Pro: что делать при перегрузке и Deadline expired

Если Nano Banana Pro вернул 503, сначала выясните, какой сервис ответил, и проверьте код ошибки. Повторяйте запрос с паузами и установленным пределом, а восстановление подтверждайте готовым изображением. Сообщение Deadline expired само по себе не делает ошибку 504.

9 мин
Проверка ключа процесса, проекта, оплаты и квоты при free_tier_requests limit 0
Руководства по API

Gemini API: free_tier_requests limit 0 на платном тарифе — что проверить и исправить

Если платный проект Gemini API получает free_tier_requests limit 0, сопоставьте ключ приложения с проектом в AI Studio, завершите требуемую настройку оплаты и проверьте действующую квоту именно той модели, которая отказала. При подтверждённом расхождении остановите повторы и обратитесь в поддержку.

11 мин