Apps Script UrlFetch 20,000 calls a day: budget Sume polling

Consumer Apps Script allows 20,000 UrlFetch calls a day and 90 minutes of trigger time. One Sume bulk run per sheet plus a slow poll fits well inside both.

5 min readSume
All posts

Google Apps Script allows 20,000 UrlFetch calls a day on a consumer account and 100,000 on Google Workspace, with 6 minutes per execution. A sheet that calls Sume once per row and polls each job every minute can spend that budget in a day. Send the rows as one Sume bulk run and poll the queue rarely, and the same sheet uses a few hundred calls.

What are the Apps Script limits that matter here?

Google's quotas page lists the numbers below. The two that bite a video sheet are the daily UrlFetch count and the total trigger runtime, because time-driven triggers are how a script polls.

Apps Script quotas, consumer vs Workspace (read 2026-10-02)
QuotaConsumerGoogle Workspace
URL Fetch calls per day20,000100,000
Script runtime per execution6 min6 min
Triggers total runtime per day90 min6 hr
URL Fetch response size50 MB per call50 MB per call
URL Fetch header size8 KB per call8 KB per call
Simultaneous executions30 per user1,000 per script

How many calls does a naive sheet use?

Take 200 rows. A per-row submit is 200 fetches. Polling each job once a minute for a 10-minute render adds about 2,000 more, so one sheet run costs about 2,200, and ten runs a day would exhaust a consumer account.

Sume's bulk run takes 1 to 100 items per request. Two requests cover 200 rows. Queue progress is one GET /v1/format-run-queues/{id}, which returns counts for the whole queue, so polling cost no longer scales with rows.

What does the cheap version look like?

Submit in chunks of 100 with a deterministic Idempotency-Key so a retried execution cannot enqueue the same sheet twice. Store the returned queue id in script properties, then let a time-driven trigger read the queue every few minutes.

A 5-minute trigger is 288 polls a day. If each poll reads one queue, that is 288 of your 20,000 fetches, and each execution lasts a second or two, far under 90 minutes of trigger runtime.

function submitChunk(rows, batchId) {
  const res = UrlFetchApp.fetch(
    'https://api.sume.com/v1/formats/acme/promo/bulk-runs', {
      method: 'post',
      contentType: 'application/json',
      muteHttpExceptions: true,
      headers: {
        Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('SUME_API_KEY'),
        'Idempotency-Key': 'sheet-' + batchId,
      },
      payload: JSON.stringify({
        concurrency: 3,
        items: rows.slice(0, 100).map(function (r) {
          return { instruction: r[0], generation_spend_cap_usd: 5 };
        }),
      }),
    });
  if (res.getResponseCode() !== 202 && res.getResponseCode() !== 200) {
    throw new Error('sume ' + res.getResponseCode() + ' ' + res.getContentText());
  }
  return JSON.parse(res.getContentText()).data.id;
}

Why poll instead of receiving Sume's webhook?

Sume signs every webhook with a sume-v1 HMAC carried in request headers, and a receiver must check it. This post does not rely on a script web app reading those headers; it polls the queue from a trigger instead, which needs no public endpoint. If you want push delivery, put a small relay in front, as in the Apps Script signature post.

Polling is a supported path. The jobs page says to keep status polling as the backup for any webhook, and to back off rather than resubmit a paid request because a local process timed out.

What does Sume not do?

Sume has no Sheets connector in these docs, and the queue has no callback of its own. The sheet owns row mapping. Use the queue items[] order, which comes back with run_id per item, to write results to the right rows, and branch on counts.failed rather than reading completed as all succeeded.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume