GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-06-21Advanced

Classifying 8,000 App Reviews Overnight with Gemini Batch API — and Moving Polling to Webhooks

Implementation notes on clearing ~8,000 backlogged app reviews from six iOS/Android apps with the Gemini Batch API in a single night — now extended with the June 2026 event-driven Webhooks that replace the morning polling step. Real cost and runtime numbers, composite-key design, hung-job triage, and deprecation discipline, with working code.

gemini-api277batch-api3webhooksindie-dev43review-classificationcost-optimization30

Premium Article

There is a particular feeling that comes from knowing a review is waiting for a reply. I am an indie developer: since 2014 I have shipped iOS and Android wallpaper and ambient apps solo, and these days I run six of them in parallel. Every morning I open App Store Connect and Google Play Console and a fresh stack of 1- and 2-star reviews, along with very specific feature requests, has appeared. Reading them one by one matters. But when I let that backlog sit across six apps for three months, it had grown to nearly 8,000 reviews. That is where this article begins.

The regular Gemini API works fine for one-by-one processing. But once cost, runtime, and the "woke up at 3am because the rate limit tripped again" overhead are taken into account, the Batch API was clearly the better fit.

What follows are the implementation notes and the real numbers from clearing the backlog in a single night — plus, added for the June 2026 changes, how the event-driven Webhooks now in the Gemini API let me delete the morning polling step entirely. This is the record of taking an overnight job from "kick it off and sleep" to "have the result pushed to me on its own."

Why I Moved Off the Regular API

I started with the regular API and a simple loop. For each review I sent the prompt plus the review body to gemini-2.5-flash and asked for structured output: { "category": "...", "severity": "...", "needs_reply": true|false }. With 500 reviews the loop worked perfectly. The trouble started somewhere around the 2,000 mark.

At Tier 1 I started hitting 429 RESOURCE_EXHAUSTED constantly. Even with asyncio throttling the concurrency, runtime became unpredictable. Twice in a row I kicked off a run before bed and woke up to a half-finished job. That is when I started looking at the Batch API seriously.

The three reasons I switched were: (1) Batch pricing is 50% of the regular API; (2) jobs are SLA-bound to finish within 24 hours, so I can stop worrying about rate limits; (3) you submit one JSONL file and wait, so there is no need to babysit progress in the middle of the night. "Kick it off and sleep" and "stay up watching it" are very different in terms of how tired you are the next day.

Implementation: JSONL to Job Submission

The implementation is small. I pull reviews out of Firestore, build a single JSONL, and submit it with the google-genai SDK.

import json
from pathlib import Path
from google import genai
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
reviews = load_reviews_from_last_quarter()  # [{ id, app_id, locale, body }, ...]
 
schema = {
    "type": "object",
    "properties": {
        "category": {
            "type": "string",
            "enum": ["bug", "feature_request", "praise", "billing", "other"],
        },
        "severity": {"type": "string", "enum": ["low", "medium", "high"]},
        "needs_reply": {"type": "boolean"},
        "summary_en": {"type": "string"},
    },
    "required": ["category", "severity", "needs_reply", "summary_en"],
}
 
# Always use a composite key (reasons below)
jsonl_path = Path("reviews_batch.jsonl")
with jsonl_path.open("w", encoding="utf-8") as f:
    for r in reviews:
        request = {
            "key": f"{r['app_id']}-{r['id']}",
            "request": {
                "contents": [
                    {
                        "role": "user",
                        "parts": [
                            {"text": f"App: {r['app_id']} / Locale: {r['locale']}\n\n{r['body']}"}
                        ],
                    }
                ],
                "generationConfig": {
                    "responseMimeType": "application/json",
                    "responseSchema": schema,
                    "temperature": 0.1,
                },
                "systemInstruction": {
                    "parts": [{"text": "You classify app store reviews for a developer."}]
                },
            },
        }
        f.write(json.dumps(request, ensure_ascii=False) + "\n")

Then upload it and create the job.

uploaded = client.files.upload(
    file=str(jsonl_path),
    config={"display_name": "reviews-2026q1-q2", "mime_type": "jsonl"},
)
 
batch_job = client.batches.create(
    model="gemini-2.5-flash",
    src=uploaded.name,
    config={"display_name": "review-classification-overnight"},
)
print(batch_job.name)  # batches/xxxx

I close the laptop and sleep. In the morning I poll with client.batches.get(name=batch_job.name). Once it reads JOB_STATE_SUCCEEDED, I download the result file with client.files.download() and write the parsed JSONL back to Firestore.

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
Replacing the 'poll it in the morning' step of an overnight batch with the event-driven Webhooks added in June 2026 — a signed receiver that downloads and writes results back automatically on completion
Side-by-side numbers from the same 8,000 reviews on the regular API vs the Batch API (half the cost, ~40% less wall time, zero failures) and why the estimate becomes easier to forecast
Composite keys that prevent silently lost results, when to give up on a hung job, empty-schema guards, and treating the 6/25 image-preview shutdown as a lesson in managing dated deprecations
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-04
Catching the Rows That Quietly Failed Overnight: A Per-Row Retry Ledger for the Gemini Batch API
A SUCCEEDED batch job is not the same as all-rows-succeeded. From running nightly batches as a solo developer, here is a per-row result ledger, a transient-vs-permanent failure classifier, selective retries, and a guard against retrying permanent failures forever, with a working SQLite state machine.
API / SDK2026-06-01
Mixing Gemini 2.5 Flash and Flash-Lite for App Store Localization
An operations log from running the same wallpaper-app store copy through both Gemini 2.5 Flash and Flash-Lite. Real cost gaps, where the lighter model breaks down, and how I now route by text type and locale.
API / SDK2026-05-18
Building a Wallpaper Variation Pipeline with Gemini 3.2 Flash Image Output — How an Indie Developer Splits the Work with Imagen 4 and Cut Monthly API Cost
An indie developer's working notes on combining Gemini 3.2 Flash Image Output with Imagen 4 to power a wallpaper-variation feature. Includes Python code, cost numbers, and three production traps from running wallpaper apps with 50M+ downloads since 2014.
📚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 →