Apps Script example · 9 min read

Retry With Backoff: Copy-Paste Apps Script Pattern

Working retry with backoff example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

UrlFetchAppBackoffResilience

Transient API failures are normal. Blind retries can stampede a service; exponential backoff spreads load.

fetchWithBackoff retries on 429 and 5xx, doubles wait from 500ms up to 15s, and honors Retry-After seconds when present.

Non-retryable 4xx fail immediately so bad auth or validation errors do not burn the attempt budget.

demoBackoffGet reads FLAKY_URL from properties to exercise the helper.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

ItemValuePurpose
HelperfetchWithBackoffReusable wrapper
maxAttempts5Default try count
Initial wait500msDoubles each retry
PropertyFLAKY_URLDemo endpoint
Retry-AfterheaderOverrides sleep when numeric

What this script does

The while loop fetches until success or attempts exhaust; retryable codes sleep then continue.

Prerequisites

UrlFetch access; an endpoint that may return 429/503 for testing.

  • muteHttpExceptions required
  • Decide which codes are retryable
  • Cap total sleep vs script runtime

Walkthrough

Point FLAKY_URL at a health endpoint, temporarily force retries by including 503 in tests, inspect Logger.

Edge cases

Retry-After may be an HTTP date — this sample only handles numeric seconds.

  • POST retries may duplicate side effects — use idempotency keys
  • Jitter can be added with Math.random

How to test

Unit-test by stubbing a function that returns codes; or hit httpstat.us/503.

Hardening for production

Add jitter; classify 408; metrics-log attempt counts to a sheet.

Variations

Reuse for Drive/Docs advanced services that throw rate limit errors.

Full code: fetchWithBackoff()

Call fetchWithBackoff(url, options, maxAttempts) anywhere you currently call UrlFetchApp.fetch.

/**
 * Exponential backoff wrapper for flaky HTTP calls.
 */
function fetchWithBackoff(url, options, maxAttempts) {
  maxAttempts = maxAttempts || 5;
  options = options || {};
  options.muteHttpExceptions = true;

  let attempt = 0;
  let waitMs = 500;
  while (attempt < maxAttempts) {
    attempt++;
    const resp = UrlFetchApp.fetch(url, options);
    const code = resp.getResponseCode();
    if (code < 400) return resp;

    const retryable = code === 429 || code === 500 || code === 502 || code === 503 || code === 504;
    if (!retryable || attempt === maxAttempts) {
      throw new Error("HTTP " + code + " after " + attempt + " attempt(s): " +
        resp.getContentText().slice(0, 300));
    }

    const retryAfter = Number(resp.getHeaders()["Retry-After"]);
    const sleepMs = !isNaN(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : waitMs;
    Utilities.sleep(sleepMs);
    waitMs = Math.min(waitMs * 2, 15000);
  }
}

function demoBackoffGet() {
  const url = PropertiesService.getScriptProperties().getProperty("FLAKY_URL");
  const resp = fetchWithBackoff(url, { method: "get", headers: { Accept: "application/json" } }, 5);
  Logger.log("OK %s bytes", resp.getContentText().length);
}
  1. Line 7: Required so 429/503 return instead of throwing.
  2. Line 17: Only transient codes enter the sleep path.
  3. Line 23: Uses server-provided delay when present.
  4. Line 26: Exponential growth capped at 15 seconds.
  5. Line 19: Surfaces the last body snippet after exhaustion.
  6. Line 30: Example caller using Script Properties.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before you run: backoff helper

  • 1Identify retryable status codes for your API
  • 2Ensure POSTs are idempotent or keyed
  • 3Cap maxAttempts so total sleep fits runtime
  • 4Log attempt count for support
  • 5Test 401 fails fast (no retry)
  • 6Set FLAKY_URL for the demo function

Frequently asked questions

Usually no — 400 means bad request. Fix the payload instead.

Randomizing sleep slightly so many clients do not retry in lockstep.

It consumes execution time, not UrlFetch quota. Still keep sleeps bounded.

Wrap fetch in try/catch for DNS-like failures and apply the same backoff.

Yes — catch errors and inspect messages for rate limit wording, then sleep/retry.

Keeps a 5-attempt loop inside typical interactive runtimes; adjust for longer triggers.

Store consecutive failure counts in CacheService and short-circuit for N minutes after meltdown.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.