GEMINI LABJP
SUNSET — The image generation models shut down today, August 17: imagen-4.0-generate-001, ultra, fast, and the Gemini 3 Image family, and calls after that fail with a hard errorSCALE — Gemini crossed one billion monthly active users on August 11ASSISTANT — Starting September 4, Gemini replaces Google Assistant on Android and Wear OS, a rollout expected to take several weeks and one you cannot reverse on a deviceDEVICES — The change covers Android phones and tablets, Wear OS watches, Assistant-enabled headphones, and phone-projected Android Auto; cars with Google built-in keep workingSPARK — Since August 13, Gemini 3.7 Flash has powered Gemini Spark for AI Pro and Ultra subscribers across more than 160 countriesPRICE — Gemini 3.7 Flash carries introductory pricing of $0.75 per million input tokens and $3.75 output through December 31, moving to $1.50 and $7.50 after thatSUNSET — The image generation models shut down today, August 17: imagen-4.0-generate-001, ultra, fast, and the Gemini 3 Image family, and calls after that fail with a hard errorSCALE — Gemini crossed one billion monthly active users on August 11ASSISTANT — Starting September 4, Gemini replaces Google Assistant on Android and Wear OS, a rollout expected to take several weeks and one you cannot reverse on a deviceDEVICES — The change covers Android phones and tablets, Wear OS watches, Assistant-enabled headphones, and phone-projected Android Auto; cars with Google built-in keep workingSPARK — Since August 13, Gemini 3.7 Flash has powered Gemini Spark for AI Pro and Ultra subscribers across more than 160 countriesPRICE — Gemini 3.7 Flash carries introductory pricing of $0.75 per million input tokens and $3.75 output through December 31, moving to $1.50 and $7.50 after that
Articles/Advanced
Advanced/2026-07-03Advanced

Your Night Batch Is Causing the Morning 429s — Priority Admission Control for a Shared Gemini Quota

When bulk jobs and interactive features share one project's RPM/TPM, the bulk lane wins by default. A priority token bucket design with measurements: 429 rate 3.2% down to 0.03%.

Gemini API213rate limitsarchitecture16token bucketoperations13production140

Premium Article

Around 8 a.m., our user-facing generation feature started throwing bursts of 429s — and only that feature. I noticed the pattern three days after I began running a nightly job that regenerates localized app descriptions in bulk. The job was scheduled for 2 a.m., but as the item count grew, its tail crept into the morning, right into the hours when real users show up. As an indie developer I tend to run several features inside a single Google Cloud project, and this shape of failure — features fighting each other for one quota — is something you will hit sooner or later if you do the same.

Gemini API rate limits (RPM and TPM) apply per model, per project. As long as everything calls from the same project, the chat feature a user is actively waiting on and the batch job nobody is waiting on draw from the same bucket. This article records how I turned that contract into a design that protects the interactive lane, with the measurements before and after.

First, Figure Out Who Actually Emptied the Bucket

429 discussions usually jump straight to retry strategy, but retries assume the quota will recover. When another feature is continuously consuming it, retries just lengthen the queue — and retry amplification makes the consumption worse. I covered how to classify retryable versus non-retryable 429s in my piece on 429 retry design by root cause; this time we start one step earlier: attribution.

The first thing I did was force every Gemini call through a thin wrapper that requires a feature tag.

import time
import threading
from collections import defaultdict, deque
from google import genai
 
client = genai.Client()
 
class TaggedGeminiClient:
    """Force a feature tag on every call and record per-minute usage."""
 
    def __init__(self, client: genai.Client):
        self._client = client
        self._lock = threading.Lock()
        # feature -> deque[(epoch_minute, requests, tokens)]
        self._usage = defaultdict(lambda: deque(maxlen=180))
 
    def generate(self, *, feature: str, model: str, contents, config=None):
        resp = self._client.models.generate_content(
            model=model, contents=contents, config=config
        )
        used = resp.usage_metadata
        total = (used.prompt_token_count or 0) + (used.candidates_token_count or 0)
        minute = int(time.time() // 60)
        with self._lock:
            bucket = self._usage[feature]
            if bucket and bucket[-1][0] == minute:
                m, r, t = bucket[-1]
                bucket[-1] = (m, r + 1, t + total)
            else:
                bucket.append((minute, 1, total))
        return resp
 
    def snapshot(self, last_minutes: int = 60):
        """Per-feature RPM / TPM over the last N minutes."""
        cutoff = int(time.time() // 60) - last_minutes
        out = {}
        with self._lock:
            for feature, buckets in self._usage.items():
                rows = [b for b in buckets if b[0] >= cutoff]
                if rows:
                    out[feature] = {
                        "avg_rpm": sum(r for _, r, _ in rows) / len(rows),
                        "peak_rpm": max(r for _, r, _ in rows),
                        "peak_tpm": max(t for _, _, t in rows),
                    }
        return out

Because it aggregates usage_metadata, you get real token counts for free. After a week of data, the picture was unambiguous: during the 7–9 a.m. peak, the bulk regeneration job owned 82% of RPM. The interactive 429s weren't caused by interactive growth at all — the batch tail had simply reached the morning.

FeatureCall patternPeak RPM shareTolerable delay
Interactive generation (user-facing)Morning/evening spikes14%Seconds — users feel it
Bulk description regenerationStarts at night, runs for hours82%Hours — nobody is waiting
Notification draft generationSporadic4%Minutes

The "tolerable delay" column is the entire design. The problem was never total capacity; it was that workloads with wildly different delay tolerance were drawing from one bucket at equal priority.

Before/After — Putting an Admission Gate in Front of Every Call

Before, each feature called the SDK directly, the obvious way:

# Before: every feature calls whenever it likes
# interactive handler
resp = client.models.generate_content(model="gemini-flash-latest", contents=user_prompt)
 
# bulk worker (loops over thousands of items)
for item in items:
    resp = client.models.generate_content(model="gemini-flash-latest", contents=build_prompt(item))

The flaw is that you only learn about congestion when the API-side rate limiter tells you — and by the time a 429 arrives, the distinction between an interactive request and a bulk request is gone. The fix is to do the traffic sorting on your side, in front of the API, with admission control:

# After: every call passes through a priority-aware gate
gate = PriorityAdmissionGate(rpm_limit=1000, tpm_limit=1_000_000,
                             reserved_interactive_ratio=0.3)
 
# interactive handler
async with gate.acquire(feature="chat", priority="interactive", est_tokens=1200):
    resp = await async_generate(user_prompt)
 
# bulk worker
async with gate.acquire(feature="bulk_regen", priority="bulk", est_tokens=2800):
    resp = await async_generate(build_prompt(item))

The gate's contract has exactly two clauses. A fixed share of capacity (30% here) is always reserved for interactive traffic. Bulk may use everything else — and may borrow the reserved share while interactive is idle — but borrowing never flows the other way.

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
Feature-tagged instrumentation that identifies which feature is eating your RPM/TPM, and how to read the result (bulk owned 82% of peak RPM in my case)
A working Python priority token bucket that reserves capacity for interactive traffic while letting bulk borrow idle headroom — controlling both RPM and TPM
Two weeks of before/after numbers (429 rate 3.2%→0.03%) plus the easy-to-miss operational fact that separate API keys do not isolate quota
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

Advanced2026-08-06
The Day the Knowledge Cutoff Moved Forward, the Stale Part Was My System Instruction
When a model's knowledge cutoff advances, the thing that goes stale is not the model — it is the dated assertions in your system instruction. Here is why only the lines written between the two cutoffs flip from helpful to contradictory, plus a working audit script and its measured results.
Advanced2026-07-09
Setting a Token Budget Per Free User: Balancing AdMob Revenue Against AI Feature Cost
Rate limits protect requests per minute. They do nothing for the invoice that arrives at the end of the month. Here is how I derive a per-user token budget from ad revenue, keep the ledger inside a single call wrapper, degrade gracefully at a soft cap, and detect abuse with one concentration ratio.
Advanced2026-07-07
Designing So the Next Shutdown Notice Doesn't Cost You an Afternoon: Isolating Gemini Behind a Single Port
The morning an image model shutdown notice landed, I couldn't say where my app touched that model. This is the design I use now: collapse Gemini dependencies into one port, with fallback and a CI deadline guard, shown as working code.
📚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 →