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-05-18Intermediate

Why Your Apps Script Stops Mid-Batch When Calling the Gemini API — UrlFetchApp Timeouts and the 6-Minute Execution Limit

When Apps Script calls the Gemini API, two limits collide: UrlFetchApp's response timeout and the 6-minute script runtime cap. Here is how to tell them apart and how I work around them with chunking, checkpoints, and time-based triggers.

Apps Script7Google Workspace15Gemini API192Troubleshooting5Timeouts

Since 2014 I have been running an indie app business — wallpaper and well-being apps that have crossed 50 million downloads in total — and a chunk of my back-office work happens in Google Sheets. A while back I wired up an Apps Script that pulls AdMob revenue rows into a sheet and asks the Gemini API to write a monthly summary per row. Fifty rows ran fine. Five hundred rows died in different places on every retry, with Address unavailable and "Exceeded maximum execution time" showing up in the log on alternate runs.

That mismatch — two error messages, one symptom — is the classic shape of this problem. There are actually two limits firing at the same time, and the first step out of the swamp is figuring out which one is biting on any given run.

Two different "timeouts" are in play

When an Apps Script project calls the Gemini API, you bump into two ceilings.

The first is the script-wide maximum execution time. Personal Google accounts get six minutes per execution; Workspace accounts get thirty. When the limit is reached the script aborts and the log shows Exceeded maximum execution time. Any rows you were halfway through will be left in an inconsistent state.

The second is the response wait on UrlFetchApp.fetch. Google does not document a hard number, but in practice the call gives up somewhere between sixty seconds and two minutes. The error surface is Exception: Address unavailable or a DNS-style failure. With a slow model like Gemini 2.5 Pro doing long-form generation or deep thinking, one request can easily cross that line.

Which one fires first depends on the size of the prompt and how long the model takes to respond, which is why the failures feel inconsistent from the outside.

Identify which limit is hitting you

Open the execution log and look at the message right before the failure.

If it says Exceeded maximum execution time, the whole script is over the six- or thirty-minute budget. Reducing the request count alone will not save you if each request still gets longer — you will just hit the wall later.

If it says Address unavailable, Exception: Request failed, or some DNS variant, you are bleeding out at the UrlFetchApp.fetch layer. That is a per-request problem, so tuning maxOutputTokens or shrinking the input still gives you room to recover.

When both errors show up across runs, I find it easier to stabilise the per-request side first and then tackle the overall runtime. Otherwise you cannot tell whether your latest tweak helped or whether the script happened to crash for the other reason.

Stabilising a single Gemini request

A handful of changes go a long way at the UrlFetchApp layer. Roughly in order of effectiveness:

function callGemini_(prompt) {
  const ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent';
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
 
  const body = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: {
      maxOutputTokens: 800,
      thinkingConfig: { thinkingBudget: 1024 },
      responseMimeType: 'application/json',
    },
  };
 
  const res = UrlFetchApp.fetch(`${ENDPOINT}?key=${apiKey}`, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(body),
    muteHttpExceptions: true,
  });
 
  if (res.getResponseCode() !== 200) {
    throw new Error(`Gemini API ${res.getResponseCode()}: ${res.getContentText().slice(0, 200)}`);
  }
  const json = JSON.parse(res.getContentText());
  return json.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
}

Three things matter here. First, cap maxOutputTokens deliberately so a single request cannot keep growing. Second, set thinkingConfig.thinkingBudget to a modest number so thinking tokens do not balloon the total latency. Third, set responseMimeType to JSON and rely on a single buffered response — Apps Script's UrlFetchApp cannot consume Server-Sent Events, so the streamGenerateContent endpoint is essentially off the table.

If a single request still exceeds about a minute, the next lever is splitting the prompt into meaningful chunks. Summarising a long document? Split by paragraph. Reviewing code? Split by file. Whatever the domain, find a natural seam and process one seam per request.

Checkpoint and resume to dodge the runtime cap

Once the per-request side is calm, the remaining problem is fitting the whole batch into six (or thirty) minutes. The pattern I landed on is to store progress in PropertiesService and use a time-based trigger to resume after each script ends.

function runBatch() {
  const props = PropertiesService.getScriptProperties();
  const startRow = Number(props.getProperty('lastRow') || '2');
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();
 
  // Step down voluntarily four minutes in, well before the hard cap
  const deadline = Date.now() + 4 * 60 * 1000;
 
  for (let row = startRow; row <= lastRow; row++) {
    if (Date.now() > deadline) {
      props.setProperty('lastRow', String(row));
      scheduleNextRun_();
      return;
    }
 
    const prompt = sheet.getRange(row, 1).getValue();
    if (!prompt) continue;
    if (sheet.getRange(row, 2).getValue()) continue; // Skip rows already filled
 
    const result = callGemini_(prompt);
    sheet.getRange(row, 2).setValue(result);
    SpreadsheetApp.flush();
  }
 
  // All rows complete — clear the checkpoint
  props.deleteProperty('lastRow');
}
 
function scheduleNextRun_() {
  // Tidy up old triggers before scheduling the next one
  ScriptApp.getProjectTriggers()
    .filter(t => t.getHandlerFunction() === 'runBatch')
    .forEach(t => ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger('runBatch').timeBased().after(60 * 1000).create();
}

Two design points matter most: write progress to the sheet and to PropertiesService on every iteration, and set your own deadline so the script steps down voluntarily before Google kills it. Letting the platform terminate you leaves cells in a half-written state, and recovery becomes a chore.

One gotcha to remember: triggers are capped at twenty per project. If you create a fresh one each run without deleting the old, you will eventually hit Triggers limit exceeded and every execution will refuse to start. Always delete before you create.

API key handling and idempotency

Two details often get overlooked on long-running batches: how you store the API key, and what happens when a run is retried.

Keep the key out of source. Store it once via PropertiesService.getScriptProperties().setProperty('GEMINI_API_KEY', '...') and you can also edit the value from the Project Settings panel in the Apps Script editor, which is handy when you rotate keys.

For idempotency, the simplest guard is the one in the sample above: skip cells that already have a value. That single line saves you from double-billing the Gemini API and from accidentally overwriting good output during a retry — both real problems if a run is interrupted partway.

Before you ship the batch job

A few habits to pick up before this kind of script goes into production:

  1. Log the response time for each request. Slow rows become obvious, and you get a data-driven case for splitting the prompt.
  2. Use muteHttpExceptions: true and inspect the response code yourself. 429 and 5xx responses are the moments to back off and retry rather than fail the whole run.
  3. Stop tests in a resumable state. If you abort from the editor, the PropertiesService checkpoint does not advance, so the next trigger run will replay rows you may not want replayed.
  4. For batches above a few thousand rows, consider moving the workload off Apps Script entirely. Cloud Functions or Cloud Run give you a longer runtime and proper streaming, and it is often less effort than fighting the 30-minute cap in Workspace.

Moving the monthly Sheets workflow to Cloud Functions was what eventually got me out of this loop personally. I now treat Apps Script as a thin glue layer that writes results back into Sheets and Docs, with the heavier work running elsewhere. If you are wrestling with the same combination of errors, I hope a few of these habits save you a weekend.

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-05-24
Why Your Gemini File API Uploads Vanish After 48 Hours — and How to Code Around It
Gemini File API resources are auto-deleted 48 hours after upload. Here is how to recognize the failure, why it happens, and concrete patterns for re-uploading, falling back to inline data, and managing expiration safely.
API / SDK2026-05-21
When responseSchema enum returns unexpected values — debugging Gemini API
Why Gemini API sometimes returns values outside the enum you defined in responseSchema, and the three-layer workaround I use in production for my wallpaper app classification pipeline.
API / SDK2026-04-28
Gemini API Won't Connect Through Corporate Proxy or SSL Verification — A Troubleshooting Walkthrough
Your Gemini API script worked on your personal laptop, but the corporate Windows machine just hangs. Isolate proxy, SSL, and certificate issues layer by layer with working Python and Node.js examples.
📚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 →