GEMINI LABJP
SUNSET — Six days until the image generation models shut down: the imagen-4.0 family and Gemini 3 Image models stop on August 17MIGRATE — gemini-3.1-flash-image is the recommended replacement, and it means rewriting generate_images calls as generate_contentCHECK — The same prompt will not necessarily produce the same picture after migrating, so secure any images you still need before the cutoffCLASSROOM — August 17 is also the day Gemini in Classroom arrives on mobile; the web rollout to students of all ages began on August 10DEPRECATION — The Grok 4.1 family shuts down on August 20, and gemini-robotics-er-1.6-preview on August 31, succeeded by the er-2 modelsCHANGELOG — The Gemini API changelog still ends at July 30. The most recent major change remains the GA of Gemini 3.6 Flash and 3.5 Flash-LiteSUNSET — Six days until the image generation models shut down: the imagen-4.0 family and Gemini 3 Image models stop on August 17MIGRATE — gemini-3.1-flash-image is the recommended replacement, and it means rewriting generate_images calls as generate_contentCHECK — The same prompt will not necessarily produce the same picture after migrating, so secure any images you still need before the cutoffCLASSROOM — August 17 is also the day Gemini in Classroom arrives on mobile; the web rollout to students of all ages began on August 10DEPRECATION — The Grok 4.1 family shuts down on August 20, and gemini-robotics-er-1.6-preview on August 31, succeeded by the er-2 modelsCHANGELOG — The Gemini API changelog still ends at July 30. The most recent major change remains the GA of Gemini 3.6 Flash and 3.5 Flash-Lite
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 API208Error Handling2Retry Design2Model Migration4Observability4

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-07-14
When a Batch Job Sat in RUNNING for Half a Day: Field Notes on Catching Stalls Early with Per-State Dwell Budgets and Record Reconciliation
When a Gemini Batch job stalls quietly under the shadow of the 24-hour SLA, per-state dwell-time budgets and submitted-vs-completed record reconciliation let you name the stall early. Field notes with real operational numbers.
API / SDK2026-06-28
The Morning a Managed Agent Stalled and Left No Trace — Building a Run-Observability Layer Outside the Sandbox
With Gemini Managed Agents, the sandbox lives on Google's side, so when a run stalls there is nothing left in your own logging stack. This is a working TypeScript design for an outside observability layer that taps stream events into a ledger, detects silent stalls, and folds runs into readable postmortems.
API / SDK2026-08-08
A Timeout Was Never Evidence of Failure — Designing Around Blocking Function Calls
When a tool call cannot return until the real-world effect finishes, two habits reverse on you: parallel dispatch and generous timeouts. Measured numbers from a sandbox harness, plus the design that replaces retries with observation.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →