◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●COMPUTER — Computer Use is now generally available in the Interactions and GenerateContent APIs (10/30)●LIVE — Live and TTS preview models shut down as early as 11/17 (15 days left). Move to gemini-3.8-live and the 3.8 TTS models●CLI — Gemini API CLI (gemini-api) is out in beta (10/27): a zero-dependency client for agents and CI●STORE — New paid projects now save Generate Content requests by default (store on) as of 10/26●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●COMPUTER — Computer Use is now generally available in the Interactions and GenerateContent APIs (10/30)●LIVE — Live and TTS preview models shut down as early as 11/17 (15 days left). Move to gemini-3.8-live and the 3.8 TTS models●CLI — Gemini API CLI (gemini-api) is out in beta (10/27): a zero-dependency client for agents and CI●STORE — New paid projects now save Generate Content requests by default (store on) as of 10/26●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-08-04Advanced

Retrying a Retired Model Never Showed Up in Success Latency

With the image generation models shutting down soon, I rebuilt a mock server to see what my retry layer actually does when it hits a retired model ID. The damage landed in wall time and queue wait, and never touched the metric I was watching.

Gemini API255Error Handling3Retry DesignModel Migration8Observability4

✦ Premium Article

August 4 was the day Gemini 3.5 Flash dropped off the model list in the global region of the Gemini Enterprise app. On August 17, the older image generation models shut down. I opened my config files to count how many places a model ID was hard-coded, and then stopped.

Counting was not the useful part.

I could not actually describe how my code would break the day after a shutdown. Calls would fail, sure. But what my retry layer would do with those failures, and what that would cost the batch as a whole, was pure guesswork.

Planning a migration on guesswork felt uncomfortable, so I reproduced the failure locally and measured it.

Permanent and Transient Errors Were Sitting in the Same Bucket

Reading back through the retry layer of a scheduled batch I keep running as an indie developer, one thing was immediately obvious.

On failure, retry with exponential backoff, up to four attempts. That was the entire policy. Nothing looked at why the call failed.

For overload (503 UNAVAILABLE) or rate limits (429 RESOURCE_EXHAUSTED), that policy is correct. Those are errors that time fixes.

A retired model is not. Wait 3.5 seconds and call again, and you get the same 404 NOT_FOUND back.

So for every model ID I failed to migrate, my batch would throw four calls it was guaranteed to fail, sleeping 3.5 seconds along the way.

The part I could not size was the actual damage. Was this "somewhat slower," or was it serious?

The Harness: Reproducing a Retired Model's Response Locally

Hammering the live API with retired model IDs is not an option, so I stood up a mock server that returns the error shapes documented in the official docs — 404 NOT_FOUND for a retired model, 503 UNAVAILABLE for overload.

Retired IDs always get a 404. Live models return 503 six percent of the time, and otherwise a 200 after a simulated 40ms of generation latency.

# server.py - a mock that reproduces retired-model responses locally
import json, random, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
 
RETIRED = {"imagen-4.0-generate-001", "gemini-3-image-preview"}
AVAILABLE = {"gemini-3.6-flash", "nano-banana-2-lite"}
rng = random.Random(20260804)   # fixed seed for reproducibility
COUNT = {"total": 0, "retired": 0, "transient": 0, "ok": 0}
LOCK = threading.Lock()
 
class H(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"   # keep-alive; without this the measurement drowns in TCP setup
    def log_message(self, *a): pass
 
    def do_POST(self):
        n = int(self.headers.get("content-length", 0))
        model = json.loads(self.rfile.read(n) or b"{}").get("model", "")
        with LOCK:
            COUNT["total"] += 1
 
        if model in RETIRED:
            with LOCK:
                COUNT["retired"] += 1
            self._send(404, {"error": {"code": 404, "status": "NOT_FOUND",
                "message": f"models/{model} is not found or no longer supported."}})
            return
 
        if rng.random() < 0.06:      # transient overload
            with LOCK:
                COUNT["transient"] += 1
            self._send(503, {"error": {"code": 503, "status": "UNAVAILABLE",
                "message": "The model is overloaded. Please try again later."}})
            return
 
        time.sleep(0.04)             # simulated generation latency
        with LOCK:
            COUNT["ok"] += 1
        self._send(200, {"model": model, "output": "ok"})
 
    def do_GET(self):
        if self.path == "/models":
            self._send(200, {"models": [{"name": f"models/{m}"} for m in sorted(AVAILABLE)]})
        else:
            self._send(404, {"error": {"code": 404, "status": "NOT_FOUND"}})
 
    def _send(self, code, obj):
        b = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("content-type", "application/json")
        self.send_header("content-length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)
 
ThreadingHTTPServer(("127.0.0.1", 8931), H).serve_forever()

One note on protocol_version. Left at the default HTTP/1.0, every call re-establishes a TCP connection, and the difference you are trying to measure disappears into connection setup. My first run produced almost no separation between the three clients, and this was why.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Measured impact of retrying permanent errors: batch wall time went from 1.56s to 8.36s under identical conditions (median of 3 runs)
✦The metric that actually moves: not success-latency p95, but queue wait p95 from submission to execution start (759ms vs 4,682ms)
✦A preflight check that drops retired model IDs at one boundary, and why you should not adopt it for speed
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.
◈ API / SDK2026-08-19
gemini-2.5-flash Can Return a 404 'no longer available' While the Docs Still List No Shutdown Date
The shutdown date for gemini-2.5-flash is blank in the official deprecation table, yet 404 reports have been circulating since July. Here is why reading that column as a safety margin misleads you, and how to check your own keys.
◈ API / SDK2026-08-17
After generated_images Disappeared: Three Branches Between the Response and a Saved File
Once you switch to generate_content, the line that breaks is usually the save. Here are the three branches on the response side - zero image parts, a shifting image count, and picking the extension - plus a receiver function you can drop in.