◉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, 6 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, 6 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-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-api286batch-api3webhooksindie-dev48review-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-05-14
Controlling thinking_budget in Gemini 2.5 Pro — Cut Costs by 70% Without Sacrificing Reasoning Quality
Leaving thinking_budget unset in Gemini 2.5 Pro leads to unexpected costs. This guide covers task-level budget design, dynamic control, and production monitoring with working Python code.
◈ API / SDK2026-08-23
Moving app AI work from runtime calls to a pre-ship batch pass
Where you put a Gemini call decides whether your request count scales with users or with assets. Here is the decision rule I used to move classification into a pre-ship batch pass, plus a resumable implementation.
◈ API / SDK2026-08-22
Once You Pass Twenty Mediation Groups, How Do You Find the Setting That Went Missing?
As ad mediation groups multiply, missing sources and type drift accumulate quietly. Here is the split I settled on: normalize the settings into one matrix, let code confirm the gaps, and send Gemini only the cells that need judgment.