◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 5 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●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●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 5 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-10-19Intermediate

When 404 and 429 Show Up in the Same Gemini API Log, Check the Model Name Before the Quota

A triage order for Gemini API logs that mix 404 NOT_FOUND and 429 RESOURCE_EXHAUSTED, plus a small Python script that separates a bad model name from a real quota limit without generating anything.

gemini-api287troubleshooting91404429model-not-foundpython109

I came across a forum post from someone building an Android app that reads photos of circuit diagrams with Gemini. The app told users the model could not be found and to check the model name in settings. Their Logcat, though, showed 404 NOT_FOUND and 429 RESOURCE_EXHAUSTED side by side. They could not tell whether the culprit was the model name, the API version, a quota, the key, or their own retry logic.

I went back to see whether anyone had answered. Nobody had. Reading it again, I realized the calls I write could produce the same pair of lines under the right conditions. As an indie developer who uses Gemini for image classification in a wallpaper app, I can't treat this as someone else's problem. So instead of guessing at an answer, I decided to settle on the order in which to check things.

The first thing to say: 404 and 429 are different questions

The official error list splits 404 into not_found and model_not_found, and 429 into rate_limit_exceeded, quota_exceeded and too_many_requests. Appearing on the same screen does not make them the same question.

What you seeWhat it is askingWill waiting fix it?
404 model_not_foundCan this key see a model with that name?No. Fix the name or your eligibility
429 rate_limit_exceededDid you send too much in a short window?Yes, it clears
429 quota_exceededIs today's allowance used up?Not until it resets

The line I drew is a single one: do not read a 429 as a quota story when the name was never resolved. If you start staring at quota graphs before the name is settled, you may be investigating a different cause entirely.

Why both can appear together: what I could confirm and what I could not

To be clear, the thread had no reply that settled the cause, so I can't claim that a wrong model name produces a false 429.

Two things I can say. If the app falls back to another model on failure, one user action turns into several requests. And those requests can fail for different reasons. An outdated primary name returns 404, while a real fallback model returns 429 because several requests were sent in a burst. Looking at only one half of the log, that looks contradictory.

So what you need is a per-line table: which request, which model name, which code. Counts won't show it.

Check the name first, without generating anything

Whether a model name exists can be checked with models.get, which generates nothing and so does not use up your generation quota. That means it is safe to send even while you are being rate limited. The script below classifies in the order name first, quota second.

import json, urllib.request, urllib.error
 
BASE = "https://generativelanguage.googleapis.com/v1beta"
 
def call(path, key, body=None):
    req = urllib.request.Request(
        f"{BASE}/{path}",
        data=json.dumps(body).encode() if body else None,
        headers={"x-goog-api-key": key, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as r:
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        try:
            return e.code, json.load(e)
        except Exception:
            return e.code, {}
 
def classify(model, key):
    # 1) Check the name only. No generation, so no quota used
    status, _ = call(f"models/{model}", key)
    if status == 404:
        return "MODEL_NAME", "Key can't see this name. Suspect spelling, generation, or access limits"
    if status in (400, 403):
        return "KEY_OR_PROJECT", f"Key or project problem ({status})"
    # 2) Only if the name resolves, probe quota with a 1-token generation
    status, body = call(
        f"models/{model}:generateContent", key,
        {"contents": [{"parts": [{"text": "ping"}]}],
         "generationConfig": {"maxOutputTokens": 1}})
    if status == 429:
        return "QUOTA", "A real limit. Wait, or revisit your allowance"
    if status == 200:
        return "OK", "The name works. Suspect the app's retry or fallback logic"
    return "OTHER", f"HTTP {status}"
 
# Example: classify("your-model-name", "YOUR_API_KEY")

Replace YOUR_API_KEY with your own key. The probe is capped at one output token so the check itself barely touches your allowance.

I checked the branches without a real key

I did not test this against the live API. I swapped in canned responses for four cases: a misspelled name, an exhausted quota, everything succeeding, and a rejected key. They came out as MODEL_NAME, QUOTA, OK and KEY_OR_PROJECT respectively.

That only shows my branching does what I intended. What the real API returns for your key is something to confirm by running it once yourself. If the result is OK, move your attention to how the app retries.

Log the requested name and the returned code on one line

To build that per-line table you need one log line per request. Nothing elaborate:

import time
 
def log_attempt(op_id, model, status):
    # Lets you line up how many calls one action made, with which model, and what came back
    print(f"{time.strftime('%H:%M:%S')} op={op_id} model={model} status={status}")

Give each user action a single op_id and you can see both that one action became three requests and that a 404 was followed by a 429. Before adding this line, all I could say was "404 and 429 both appear." With it, I can say "the primary is the 404, the fallback is the 429."

If it still isn't fixed

If you get OK but the app keeps failing, suspect the number of calls. Temporarily turn off the fall-back-to-another-model path and count how many requests a single user action sends. If the name is not visible, note that a model can stop working even when the official table lists no shutdown date, as in this 2.5 Flash 404 case. When the model list returns 200 but something else blocks you, the 403 triage order is a useful companion.

For the kind of 429 that waiting never fixes, I wrote out the retry design with working code in a premium article.

Your next step: take one model name that returned 404 in your logs and pass it to the script above. Name first, quota second. That is the order I intend to keep the next time I get stuck.

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 →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-05-17
Diagnosing INVALID_ARGUMENT Errors in Gemini API Batch Image Analysis
When using the Gemini API to analyze multiple images at once, INVALID_ARGUMENT errors can be surprisingly hard to diagnose. This guide covers the three root causes—MIME type mismatches, inline data size limits, and contents structure errors—with code to fix each one.
◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.