AI Image Generation API Error Handling — Retry, Timeout & Fallbacks

Sep 7, 2026·8 min read

You've built a feature that generates product images on demand. A user clicks "Generate" — your backend calls the image API — and instead of an image, you receive a 503 Service Unavailable. You return nothing, the user sees a broken state, and you have no recovery path.

This guide covers production-grade error handling for AI image generation APIs: the HTTP status codes you'll actually encounter, retry patterns with exponential backoff, per-model timeout budgets, and model fallback logic for graceful degradation. All examples target the GenRelay API with Nano Banana Pro, Nano Banana 2, and GPT-image-2.

What HTTP Error Codes Do AI Image APIs Return?

GenRelay image endpoints return standard HTTP status codes with structured JSON error bodies. As of Q3 2026, the error taxonomy is:

Status Code Meaning Retry?
400 invalid_request Bad parameter — prompt too long, unsupported resolution No
401 invalid_api_key Missing or expired API key No
402 insufficient_credits Account balance below generation cost No
422 content_policy Prompt or reference image violates content policy No
429 rate_limit_exceeded Concurrent or per-minute limit hit Yes (after backoff)
500 model_error Transient model inference failure Yes (1–2 times)
503 service_unavailable Temporary overload or deployment in progress Yes (after backoff)
524 gateway_timeout Generation exceeded the gateway's upstream timeout Yes (once)

Non-retriable errors (4xx except 429) always indicate a client-side problem — a bad prompt, a wrong key, insufficient credits. Log the error body, surface the message to your alerting system, and do not retry. Retrying a 400 or 422 wastes your quota and adds latency with no chance of success.

Retriable errors (429, 500, 503, 524) reflect transient infrastructure state. Use exponential backoff with jitter so retrying clients don't all hammer the API in sync.

How Should I Implement Retry Logic?

Exponential backoff with jitter is the standard pattern. It spreads retry load over time and prevents multiple clients from hitting the API in synchronized bursts after a brief outage.

import requests
import time
import random
import logging

logger = logging.getLogger(__name__)

RETRIABLE_STATUS = {429, 500, 503, 524}

def generate_image_with_retry(
    prompt: str,
    model: str = "nano-banana-pro",
    size: str = "1024x1024",
    max_retries: int = 3,
    base_delay: float = 1.5,
    api_key: str = "",
) -> dict:
    url = "https://genrelay.ai/v1/images/generations"
    headers = {"Authorization": f"Bearer {api_key}"}
    payload = {"model": model, "prompt": prompt, "size": size}

    for attempt in range(max_retries + 1):
        try:
            resp = requests.post(url, headers=headers, json=payload, timeout=(5, 55))
        except requests.Timeout:
            if attempt == max_retries:
                raise
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1.0)
            logger.warning("Timeout on attempt %d, retrying in %.1fs", attempt + 1, delay)
            time.sleep(delay)
            continue

        if resp.status_code == 200:
            return resp.json()

        # Surface non-retriable errors immediately — no retry budget spent
        if resp.status_code not in RETRIABLE_STATUS or attempt == max_retries:
            resp.raise_for_status()

        # Respect Retry-After header when present (common on 429)
        retry_after = resp.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else (
            base_delay * (2 ** attempt) + random.uniform(0, 1.0)
        )

        logger.warning(
            "HTTP %d on attempt %d, retrying in %.1fs — %s",
            resp.status_code, attempt + 1, delay,
            resp.json().get("error", {}).get("message", ""),
        )
        time.sleep(delay)

    raise RuntimeError("Exhausted all retries without a successful response")

Key design decisions:
- Retry-After header is respected when present. The GenRelay API sends this on 429 responses; honoring it avoids getting rate-limited again immediately.
- Connection timeouts (requests.Timeout) are retried — a dropped connection mid-request is a network-level transient failure, not a client error.
- Non-retriable status codes call raise_for_status() immediately, letting callers handle them without burning retry budget.
- base_delay * (2 ** attempt) + jitter gives delays of roughly 1.5s, 3.5s, and 7.5s across three retries — enough time for most transient failures to clear.

What Timeout Should I Set Per Model?

GenRelay image generation is synchronous: the HTTP response body arrives once the image is fully generated. Your client timeout must be long enough to cover both inference and network transfer.

Model Resolution Typical latency Recommended client timeout
GPT-image-2 1024×1024 5–15 s 45 s
Nano Banana 2 1024×1024 3–10 s 35 s
Nano Banana Pro 1024×1024 4–12 s 40 s
Nano Banana Pro 2048×2048 6–18 s 50 s
Nano Banana Pro 4096×4096 10–28 s 65 s

Set timeout as a (connect_timeout, read_timeout) tuple rather than a single integer — this prevents worker threads from blocking indefinitely on hung connections:

resp = requests.post(
    "https://genrelay.ai/v1/images/generations",
    headers={"Authorization": f"Bearer {api_key}"},
    json={"model": "nano-banana-pro", "prompt": prompt, "size": "1024x1024"},
    timeout=(5, 45),  # 5 s to establish connection, 45 s to receive full body
)

A 524 Gateway Timeout means GenRelay's upstream timeout fired before the model responded. Retry once — transient inference queue congestion usually clears within a few seconds. If 524s persist across many requests, your resolution tier or prompt complexity may be pushing generation time past the gateway limit; try a lower resolution or a shorter prompt.

How Do I Implement Model Fallbacks?

A fallback chain lets your application degrade gracefully when the primary model is temporarily unavailable or when you need to optimize cost during high-traffic periods.

Quality-first fallback (start with the highest-capability model, step down on repeated failure):

import requests
import logging

logger = logging.getLogger(__name__)

# Ordered from highest quality/cost to lowest
FALLBACK_CHAIN = [
    {"model": "nano-banana-pro", "size": "1024x1024"},  # $0.030 at 1K
    {"model": "nano-banana-2",   "size": "1024x1024"},  # $0.020 at 1K
    {"model": "gpt-image-2",     "size": "1024x1024"},  # $0.014
]

NON_RETRIABLE = {400, 401, 402, 422}

def generate_with_fallback(prompt: str, api_key: str) -> dict:
    url = "https://genrelay.ai/v1/images/generations"
    headers = {"Authorization": f"Bearer {api_key}"}

    for config in FALLBACK_CHAIN:
        try:
            resp = requests.post(
                url,
                headers=headers,
                json={"prompt": prompt, **config},
                timeout=(5, 50),
            )
            if resp.status_code == 200:
                result = resp.json()
                logger.info("Generated with model: %s", config["model"])
                return result
            if resp.status_code in NON_RETRIABLE:
                resp.raise_for_status()  # don't try next model — client error
            logger.warning("Model %s returned %d, trying next", config["model"], resp.status_code)
        except requests.Timeout:
            logger.warning("Model %s timed out, trying next", config["model"])

    raise RuntimeError("All fallback models failed")

Cost-first fallback: reverse the chain — GPT-image-2 → Nano Banana 2 → Nano Banana Pro. Useful when your primary concern is per-image cost and quality is secondary.

For production, combine fallback with your retry function: retry the primary model 1–2 times before moving to the next in the chain, rather than failing over immediately on the first error.

The model field in successful API responses reflects which model actually ran — log this to track how often each fallback tier activates. A sustained spike in fallback usage is a signal to open a support ticket for quota or concurrency increases.

How Do I Handle Content Policy Rejections?

A 422 content_policy error means the prompt or reference image was rejected during pre-screening — no generation occurred and no credits were consumed. These should not be retried; the same input will produce the same rejection.

if resp.status_code == 422:
    error = resp.json().get("error", {})
    if error.get("code") == "content_policy":
        logger.warning(
            "Content policy rejection",
            extra={"prompt": payload["prompt"], "detail": error.get("message")},
        )
        return {"error": "content_policy", "image_url": None}
    resp.raise_for_status()

Return a structured null result to your frontend rather than propagating a 500 to end users. Log the prompt and rejection reason for downstream review — if a model of legitimate prompts triggers policy rejections, the content guidelines section of the GenRelay docs lists what's filtered.

Cost Impact of Retry and Fallback

A few cost considerations worth knowing before shipping:

  • Failed requests are not billed. A request is only charged when the API returns a 200 with a generated image. Retried 429s, 500s, and timeouts do not consume credits.
  • Fallback to cheaper models costs less per image. If Nano Banana Pro ($0.030 at 1K) is temporarily unavailable and you fall through to GPT-image-2 ($0.014), the fallback generation costs 53% less. Build this into your COGS tracking.
  • Retry loops can inflate cost in content policy edge cases. If you retry a 422, you won't be charged — but you will waste wall-clock time. Filter clearly non-retriable codes out of your retry loop.

For further reading on API authentication setup and key rotation, see the image generation API authentication guide. For parallel generation patterns, see batch image generation via API.

FAQ

Does GenRelay charge credits for requests that fail with 5xx errors?
No. Credits are only deducted on a successful 200 response. Requests that return 500, 503, or 524 are not billed.

Should I retry 500 errors, and how many times?
Yes — once or twice with a 2–5 second delay. A 500 from GenRelay typically reflects a transient model inference failure; the same prompt usually succeeds immediately on retry. If 500 errors persist across many sequential requests, check the GenRelay status page for an active incident.

What's the concurrent request limit for image generation?
GenRelay free tier allows up to 5 concurrent image requests. Paid plans allow higher concurrency; contact support for exact per-plan limits or to request an increase.

Can I detect which model ran in a fallback scenario?
Yes. The 200 response body includes a model field that reflects the model that executed the generation, not necessarily the model you requested. Log this field in production to monitor fallback activation rates.

How do I set up alerts for error rate spikes?
Instrument response status codes in your metrics layer (Prometheus, Datadog, CloudWatch). Alert when rate_limit_exceeded exceeds 5% of requests per minute, or when model_error exceeds 2%. Sustained spikes in either indicate you need higher concurrency allowances or a quota review.

Related posts

Join our DiscordAI Image Generation API Error Handling — Retry, Timeout & Fallbacks — GenRelay