GEMINI LABJP
REGION — Gemini 3.5 Flash is being removed from the global region in the Gemini Enterprise app today. Setups that never pinned a region will see the option quietly changeIMAGE — Imagen 4 and Gemini 3 Image generation models shut down on August 17. That leaves about two weeks to migrateROBOTICS — The gemini-robotics-er-1.6-preview model shuts down on August 31. Preview models are best treated as needing a fallback path from the startSAMPLING — On Gemini 3.6 Flash, custom temperature, top-K, and top-P values are ignored, while frequency and presence penalties now raise an API errorFLASH — Gemini 3.6 Flash and 3.5 Flash-Lite reached general availability. 3.6 Flash improves token efficiency and agentic planning at a lower price than 3.5 FlashAGENTS — Managed Agents in the Gemini API gained 3.6 Flash and hooks, giving official support for injecting logic partway through an agent runREGION — Gemini 3.5 Flash is being removed from the global region in the Gemini Enterprise app today. Setups that never pinned a region will see the option quietly changeIMAGE — Imagen 4 and Gemini 3 Image generation models shut down on August 17. That leaves about two weeks to migrateROBOTICS — The gemini-robotics-er-1.6-preview model shuts down on August 31. Preview models are best treated as needing a fallback path from the startSAMPLING — On Gemini 3.6 Flash, custom temperature, top-K, and top-P values are ignored, while frequency and presence penalties now raise an API errorFLASH — Gemini 3.6 Flash and 3.5 Flash-Lite reached general availability. 3.6 Flash improves token efficiency and agentic planning at a lower price than 3.5 FlashAGENTS — Managed Agents in the Gemini API gained 3.6 Flash and hooks, giving official support for injecting logic partway through an agent run
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 API203Error 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-03
Measuring Update Policies for Memory Profiles: The Guard That Cost Me 16 Points of Accuracy
Memory profiles went GA in Memory Bank, making structured memory available to downstream code. I built three update policies and compared them under identical conditions. The one that looked obviously correct turned out to be the worst. Full harness code and the path to per-field TTLs.
📚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 →