If a Nano Banana image request fails, preserve the raw evidence before retrying. The decisive question is which layer produced the signal: 400/429/503 are request-level HTTP results, NO_IMAGE is an official generation finish reason in the current Google API reference, and a timeout can mean only that your caller or gateway stopped waiting. Those branches need different actions.
| Symptom | Evidence to capture first | First action | Retry unchanged? | Recovery proof |
|---|---|---|---|---|
400 INVALID_ARGUMENT | Final URL, API version, model, redacted body, error details | Correct the request contract | No | The same minimal request no longer returns 400 |
429 RESOURCE_EXHAUSTED | Canonical status, details, current rate-limit view, attempt and time | Identify the constrained dimension and smooth traffic | Bounded only | Stable success under controlled concurrency |
503 UNAVAILABLE | Request ID, model, route, start/end time | Check service health and back off | Bounded only | Same-route minimal probe succeeds |
finishReason=NO_IMAGE | Full candidate, finishMessage, parts, response ID | Check output modality and run a minimal probe | Classify first | A new response contains a decodable image part |
Timeout or 504 | Per-hop deadlines, elapsed time, whether a body arrived, request ID | Find the first closing layer and check unknown outcome | Transient branches only | Same-route completion without duplicate output |
Google's current Gemini API troubleshooting guide recommends bounded exponential backoff for transient errors such as 408, 429, and 5xx, while 400/403 should not be retried blindly. The API errors reference lists no_image as a generation error, and the GenerateContent reference defines NO_IMAGE as a FinishReason: an image was expected, but none was generated. It is official, but it is not an HTTP status and does not by itself prove safety filtering, overload, or billing behavior.
Preserve the evidence before a wrapper flattens it
Many incidents become untraceable because an adapter turns a detailed response into one exception string. Log the following fields before applying user-facing error mapping, with prompts, input images, and credentials redacted:
texttimestamp_utc, provider, base_url, api_version, endpoint, model http_status, canonical_status, request_id, upstream_request_id attempt, started_at, latency_ms, client_timeout_ms candidate_count, prompt_block_reason, finish_reason part_mime_types, has_text_part, has_image_part, error_body_summary
“Nano Banana” is the common nickname, not a protocol identifier. Record the actual route. A Gemini Developer API call, a Vertex AI call, a third-party gateway, and your own reverse proxy can produce similar surface symptoms with different responsibility boundaries. This guide uses the Gemini Developer API's native models/{model}:generateContent shape unless a section explicitly says otherwise.
Use one harmless, single-goal image request as the same-route probe. Do not copy a model ID from an old article; set GEMINI_MODEL to an image model that the current official model page shows as available for your account.
bashAPI_VERSION="${GEMINI_API_VERSION:-v1beta}" MODEL="${GEMINI_MODEL:?set GEMINI_MODEL}" curl --fail-with-body --silent --show-error \ -X POST \ "https://generativelanguage.googleapis.com/${API_VERSION}/models/${MODEL}:generateContent" \ -H "x-goog-api-key: ${GEMINI_API_KEY:?set GEMINI_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "contents": [{"parts": [{"text": "Generate a product photo of a blue ceramic mug on a white background."}]}], "generationConfig": {"responseModalities": ["IMAGE"]} }'
If this probe succeeds while the original request fails, the fault domain has narrowed to input, context, or optional parameters. If it also fails, continue with account, capacity, route, and platform evidence.

A 400 is a diff problem, not a retry problem
400 INVALID_ARGUMENT means the server rejected the request contract. Common categories include malformed JSON, a field unsupported by the selected API version, a model that does not support the requested feature, or invalid image input metadata. Sending the identical body again only adds noise.
Reduce the request in a controlled order:
- Record the final URL after configuration is resolved. Confirm that a native Gemini Developer API path was not mixed with a Vertex project/region path.
- Compare the model, API version, output modalities, and image options against the current image generation documentation.
- Remove optional fields until only one text part and an
IMAGEoutput request remain. - Add one field back per test. A successful minimal request plus one failing addition is stronger evidence than a large “correct-looking” body.
- For image inputs, log the actual MIME type, transfer method, and byte size rather than trusting the filename extension.
Do not mark the incident fixed because the error message changed. The proof is a response from the same endpoint that contains a valid image part.
Diagnose 429 and 503 separately, then share the retry machinery
A 429 RESOURCE_EXHAUSTED means one of the applicable usage or rate constraints has been exceeded. The relevant dimension can include RPM, TPM, RPD, spend, or another model/tier limit. Those values change. Check the current values in Google AI Studio's rate-limit view instead of relying on a number copied into application code or documentation.
Look for bursts before requesting more quota. Queue work, cap worker concurrency, and spread requests over time. If many workers wake on the same delay, they can create a retry storm even after capacity returns.
A 503 UNAVAILABLE means the service may be overloaded or temporarily interrupted. It is not evidence that the prompt is invalid, and there is no universal recovery window. Compare a minimal probe on the same account/model/route, inspect official service health, and stop automated retries when the application's attempt or elapsed-time budget is exhausted. Persistent 503s need a request ID and time window for provider support.
Use one retry layer. An SDK may already retry transient failures; stacking another loop outside it can multiply calls unexpectedly. For custom REST clients, bound the per-attempt timeout, attempt count, and total elapsed budget:
pythonimport random import time import requests TRANSIENT_STATUS = {408, 429, 500, 502, 503, 504} def post_with_bounded_retry( url, headers, payload, *, max_attempts=4, total_budget_s=120 ): deadline = time.monotonic() + total_budget_s for attempt in range(1, max_attempts + 1): remaining = deadline - time.monotonic() if remaining <= 0: raise TimeoutError("retry budget exhausted") try: response = requests.post( url, headers=headers, json=payload, timeout=(10, min(60, remaining)), ) except (requests.Timeout, requests.ConnectionError): if attempt == max_attempts: raise else: if response.status_code == 400: raise ValueError(f"fix request before retry: {response.text[:500]}") if response.status_code not in TRANSIENT_STATUS: response.raise_for_status() return response.json() if attempt == max_attempts: response.raise_for_status() base = min(2 ** (attempt - 1), 30) delay = base + random.uniform(0, base * 0.5) if delay >= deadline - time.monotonic(): raise TimeoutError("no retry fits inside total budget") time.sleep(delay) raise RuntimeError("unreachable")
The exact cap is an application decision, not a provider promise. Bound both attempts and total elapsed time so a retry cannot outlive the user's request or duplicate work after the caller has moved on.
NO_IMAGE is a generation result, not an HTTP error
Do not conflate the official finish reason with an application's generic “no image” message. In the current Google contract, NO_IMAGE belongs to a candidate's finishReason; a gateway may separately map it to lowercase no_image. Preserve the upstream body and walk it from the outside in:
- No
candidates: inspectpromptFeedback. A prompt problem can prevent candidates from being returned. finishReason=NO_IMAGE: recordfinishMessage,responseId, andmodelVersion, confirm the requested output modality, and run the harmless same-route probe. Do not relabel it as safety or 503 without evidence.IMAGE_SAFETY,IMAGE_PROHIBITED_CONTENT, or another policy finish reason: review a legitimate request or accept the boundary; retries are not a bypass.- Text parts but no image part: read the text and verify output modalities. Text-only output is not the same evidence as
NO_IMAGE. - An
inlineDataimage part exists but the app still says no image: inspect adapter parsing, MIME validation, base64 decoding, and storage writes.
A defensive parser should keep image success, official NO_IMAGE, policy stops, and other image-less candidates separate:
pythondef inspect_generate_content(body): feedback = body.get("promptFeedback") candidates = body.get("candidates") or [] if not candidates: return {"kind": "no_candidates", "promptFeedback": feedback} candidate = candidates[0] finish_reason = candidate.get("finishReason") parts = ((candidate.get("content") or {}).get("parts") or []) images = [part["inlineData"] for part in parts if part.get("inlineData")] texts = [part["text"] for part in parts if part.get("text")] if images: kind = "image" elif finish_reason == "NO_IMAGE": kind = "no_image" elif finish_reason in { "SAFETY", "IMAGE_SAFETY", "PROHIBITED_CONTENT", "IMAGE_PROHIBITED_CONTENT", "IMAGE_RECITATION", }: kind = "blocked_or_policy" else: kind = "candidate_without_image" return { "kind": kind, "finishReason": finish_reason, "finishMessage": candidate.get("finishMessage"), "imageCount": len(images), "textCount": len(texts), "responseId": body.get("responseId"), "modelVersion": body.get("modelVersion"), }
For a deeper response-side branch, see the Gemini no-image and IMAGE_SAFETY guide. If authentication and other Gemini HTTP errors are mixed into the incident, use the broader Gemini API error troubleshooting guide.
Timeouts: identify the first layer that closed
A typical request crosses several deadlines:
textclient/SDK → application server → reverse proxy/API gateway → provider → model
A client cancellation may leave no Google HTTP body at all. A reverse proxy can generate its own 504, so record a provider 504 only when it arrived in the upstream response. The inverse matters too: a caller timeout does not prove the server stopped. Before retrying work that can create an image or downstream write, query task records or artifact storage by request/response ID so you do not duplicate an output. Track start time, time to first byte, end time, configured deadline, and closing layer for every hop you control.
Run two comparisons through the same account, route, and model: the harmless minimal probe and the original complex input. If the probe succeeds and the complex request consistently ends at a client deadline, change the correct timeout or reduce the request. If both fail with the same 503/504 window, repeatedly sending the larger request adds little diagnostic value.
Raising every timeout is not a fix. It can merely make users wait longer while an upstream capacity issue, proxy idle timeout, or stuck worker remains unchanged.

Stop conditions and the support packet
Stop automatic retries when the response is a 400 or another known non-retryable client error, when NO_IMAGE has not yet been classified with a minimal probe and raw response, when a safety/policy finish reason is present, when the attempt or elapsed-time budget is exhausted, when an identical request fingerprint keeps returning 5xx, or when the caller can no longer use the result.
Send support a redacted incident packet instead of a screenshot of the final UI message:
- UTC time window, provider/base URL, API version, endpoint, and model;
- HTTP code, canonical status, and full redacted error details;
- request ID and upstream request ID;
- per-attempt latency, total elapsed time, and timeout configuration;
- minimal safe prompt, input-image MIME information, and output modalities;
- candidate count,
promptFeedback,finishReason,finishMessage, and part MIME types; responseId,modelVersion, and whether a timed-out call later produced an artifact;- result of the same-account, same-route, same-model minimal probe.
The final verification is narrow and observable: the minimal request returns a decodable image part on the route you actually use, then the original workload completes under controlled concurrency. A 200 status, a queue marked complete, or the disappearance of an adapter's NO_IMAGE label is not sufficient by itself.



