Apps Script UrlFetch 20,000/day and 6 min: size a Sume sheet run

Apps Script allows 20,000 UrlFetch calls a day and 6 minutes per run on a consumer account. How to size a Sume sheet batch inside both limits.

5 min readSume
All posts

A consumer Google account can make 20,000 UrlFetch calls a day from Apps Script, and one execution stops at 6 minutes. A Sume sheet run fits easily if each row costs one submit call with mode: "async" and you read the result later, and it breaks if every row waits for its video inside the same execution.

This post sizes a batch against the limits on Google's own quota page, then shows the submit loop that stays inside them. For the full sheet-to-video build, start with Google Sheets to video automation with Apps Script and Sume.

What are the Apps Script limits that matter for an API loop?

Google lists the limits per account type, and says quotas reset 24 hours after the first request and can change without notice. Check the page before you size a production batch.

Apps Script quotas, read 2026-10-02
QuotaConsumer accountGoogle Workspace account
URL Fetch calls per day20,000100,000
Script runtime per execution6 min6 min
Triggers total runtime per day90 min6 hr
Simultaneous executions per user3030
URL Fetch response size50 MB per call50 MB per call

Why does a sync wait on every row break the 6-minute limit?

Sume's sync mode holds the HTTP request for up to wait_timeout_seconds, which is capped at 30 seconds. Ten rows that each wait the full 30 seconds already use 5 of your 6 minutes, and video jobs usually outlast that wait anyway. The jobs and results docs say a timed-out wait still returns a job id and that you must poll rather than resubmit.

So the sheet should never wait. Submit with mode: "async", write the job id into a column, and let a second, cheap trigger read status. The job keeps running whether or not your script is alive.

How do I budget the 20,000 daily calls?

Count calls per row: one submit, plus the status reads you make until the job is terminal. The next_poll_after_seconds field on the envelope tells you when to look again, so you do not need a tight loop.

As an illustration only, 100 rows with one submit and 15 status reads each is 1,600 calls, about 8 percent of the consumer day. The real count depends on your models and how often you poll, so measure one row first.

If the 90-minute trigger budget is the tighter limit, move the polling off Apps Script: a Sume webhook delivers the terminal event to a URL you control, and 10 attempts at a 30-second default spacing cover short outages.

What does the submit loop look like?

This reads rows with no job id, submits each with an Idempotency-Key built from the row number, and stores the returned job id. A re-run after a timeout returns the original job rather than billing a second one, because Sume reuses the job for the same key. It assumes the key is in Script Properties as SUME_API_KEY and that column A holds the prompt and column B the job id.

function submitRows() {
  const key = PropertiesService.getScriptProperties().getProperty('SUME_API_KEY');
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  for (let i = 1; i < rows.length; i++) {
    if (!rows[i][0] || rows[i][1]) continue;
    const res = UrlFetchApp.fetch('https://api.sume.com/v1/image-1.0/generate', {
      method: 'post',
      contentType: 'application/json',
      muteHttpExceptions: true,
      headers: { Authorization: 'Bearer ' + key, 'Idempotency-Key': 'sheet-row-' + (i + 1) },
      payload: JSON.stringify({ prompt: rows[i][0], mode: 'async' }),
    });
    if (res.getResponseCode() === 429) break; // back off, retry on the next trigger
    const body = JSON.parse(res.getContentText());
    if (body.request_id) sheet.getRange(i + 1, 2).setValue(body.request_id);
  }
}

What happens when a run hits the 6-minute wall?

Apps Script stops the execution, and nothing about that stops the jobs you already submitted. That is the useful property of async submits: a job is durable the moment Sume returns its id, so a half-finished loop loses only the rows it never reached. The Idempotency-Key per row means the next trigger can start from the top and the rows already submitted return their original jobs instead of new paid ones.

Two habits keep this clean. First, write the job id to the sheet immediately after each submit, not at the end of the loop. Second, stop the loop yourself at about five minutes using Date.now() so the script ends on your terms rather than by a timeout. Sume's docs are clear that a client-side timeout does not cancel the job; it only stops you watching it, and the job keeps billing until it finishes or you cancel it with POST /v1/jobs/{id}/cancel before generation starts.

When should I use a Format bulk queue instead?

If every row runs the same saved recipe, POST /v1/formats/{handle}/{slug}/bulk-runs takes 1 to 100 items with a concurrency window of 1 to 16 and returns one queue id. You then poll GET /v1/format-run-queues/{id} once instead of once per row, which saves UrlFetch calls. The details are in bulk runs and Format bulk runs for 100 renders.

Sume does not push progress into the sheet for you. The queue has no webhook of its own, so something on your side has to poll or receive per-item webhooks.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume