GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is now available as the fastest, most cost-efficient Gemini Image modelOMNIFLASH — Gemini Omni Flash comes to the API in public preview for building custom, dynamic video workflowsAGENTS — Managed Agents arrives in the Gemini API, running autonomous agents in secure Google-hosted Linux sandboxesFILESEARCH — File Search now supports multimodal search via gemini-embedding-2, with media_id for visual citationsWEBHOOKS — Event-driven Webhooks replace polling for the Batch API and long-running operationsDEPRECATE — Older image generation models are deprecated and shut down on Aug 17, so plan your migrationNANOLITE — Nano Banana 2 Lite is now available as the fastest, most cost-efficient Gemini Image modelOMNIFLASH — Gemini Omni Flash comes to the API in public preview for building custom, dynamic video workflowsAGENTS — Managed Agents arrives in the Gemini API, running autonomous agents in secure Google-hosted Linux sandboxesFILESEARCH — File Search now supports multimodal search via gemini-embedding-2, with media_id for visual citationsWEBHOOKS — Event-driven Webhooks replace polling for the Batch API and long-running operationsDEPRECATE — Older image generation models are deprecated and shut down on Aug 17, so plan your migration
Articles/Workspace
Workspace/2026-07-13Advanced

Retiring the Poll That Waits on an Overnight Batch — An Apps Script doPost Sink for Gemini Signals

Polling a Gemini batch or long-running operation every five minutes from an Apps Script time trigger quietly stacks up UrlFetch calls and latency. Receive the webhook in doPost, treat it as an unverified signal, then confirm authoritatively and apply idempotently.

Apps Script6Gemini API181Webhook3Batch API4Google Workspace15

Premium Article

You send a batch to Gemini overnight, and the next morning the results are written back into a spreadsheet. Running several apps solo, these "let it work while I sleep" jobs pile up naturally. On my own side, I've leaned on Apps Script time-driven triggers to have Gemini classify app reviews and roll up some AdMob figures overnight.

The way I waited for them, though, always nagged at me. To learn whether a job had finished, a trigger woke every five minutes and asked the API. Most of the time the only answer was "not yet." This week, with Gemini's Batch API and long-running operations gaining webhook support, I could finally fold that wasted polling away. This article builds the design that puts the receiving end in an Apps Script doPost, one stumbling point at a time.

The trigger: five-minute polling stacks up quietly

Here is the shape polling usually takes: a time trigger runs every five minutes and asks about any operation that is still open.

// Runs on a 5-minute time-driven trigger (the old way)
function pollPendingOperations() {
  const props = PropertiesService.getScriptProperties();
  const pending = JSON.parse(props.getProperty('pending_ops') || '[]');
  for (const opName of pending) {
    const res = UrlFetchApp.fetch(
      'https://generativelanguage.googleapis.com/v1beta/' + opName,
      { headers: { 'x-goog-api-key': getApiKey_() }, muteHttpExceptions: true }
    );
    const op = JSON.parse(res.getContentText());
    if (op.done) applyResult_(opName, op);  // apply if finished
  }
}

The code works fine. The problem is the cost-to-value ratio. A five-minute cadence is 288 invocations a day, and most of them are spent on empty UrlFetch calls that say "still running." If a batch finishes in about 40 minutes, roughly eight of those checks touch it and only the last one matters. The other seven exist only to mark time.

Apps Script caps UrlFetchApp at a daily quota — around 20,000 calls on a consumer account. Letting polling alone eat hundreds of them quietly hurts when you share that budget across other automations. And above all, up to five minutes of latency sits between completion and apply.

Treat the webhook as an unverified signal, not a trusted notification

"Webhook support" invites a tempting assumption: the completion event arrives, so just write its contents straight through. When you receive it in Apps Script, though, this is the fork in the design.

An Apps Script Web App — that is, doPost(e) — cannot read arbitrary HTTP request headers. What you get is mainly the body (e.postData.contents) and the query string (e.parameter). Most webhook senders carry a signature in a dedicated header, and doPost has no way to verify it. This is a fact tucked into the corner of the docs, and the kind of trap you only notice once you actually start building the endpoint.

So this article treats the webhook not as a trusted notification but as an unverified signal that "something apparently finished." When the signal arrives, we don't take it at face value; we go and confirm with the API authoritatively. That single extra step makes the inability to verify headers a non-issue.

HandlingAssumptionFit with Apps Script
Trust the notification, apply immediatelyYou can verify the header signatureNo (headers are unreadable)
Receive as a signal, confirm via the APICompletion is fetched againGood (this article's approach)

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
See how five-minute polling wastes about 288 UrlFetch calls a day, and how a webhook collapses that to near zero while cutting completion-to-apply latency from minutes to seconds
Confront the fact that Apps Script doPost cannot read arbitrary request headers, and build a sink that treats the webhook as an unverified signal rather than a trusted notification, with complete code
Combine an operations.get authoritative check, an idempotency ledger, and a low-frequency reconciliation sweep to absorb both duplicate deliveries and missed events
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

Workspace2026-07-08
Your Apps Script Gemini Automation Fails Every Month-End — Budgeting Against the UrlFetch Daily Quota
Apps Script automations that call Gemini stall on the UrlFetch daily call quota — a separate ceiling from the 6-minute limit and trigger counts. Here is a daily budget governor with backlog carry-over that keeps the job running on busy days, with working code and a verified simulation.
Workspace2026-07-01
When Two Triggers Write at Once, Your Gemini Result Quietly Vanishes — A Durable Result Store for Apps Script
Storing Gemini results from several Apps Script triggers loses writes through read-modify-write races and PropertiesService size limits. Build a result store that survives, using LockService, a durable sink, and idempotency keys.
Workspace2026-06-29
Keeping Apps Script + Gemini Automations on Least Privilege: Explicit Scopes and Catching Scope Creep
Apps Script automations that call Gemini quietly accumulate OAuth scopes. Here is how to declare explicit scopes in appsscript.json, catch scope creep in CI, and avoid forcing every user to re-consent.
📚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 →