Flaky APIs and 429s are part of bootstrapper life. This helper wraps UrlFetchApp.fetch with exponential backoff so a single blip doesn't fail the whole job.
What you'll need
- A URL (and optional
optionsobject) you'd normally pass toUrlFetchApp.fetch - Permission for the script to call external services
- A sense of which failures are worth retrying (429 / 5xx / transient network errors — not "404 forever")
Why backoff instead of a tight loop
Hammering the same endpoint every 50ms makes rate limits worse. Exponential backoff waits longer after each failure (~0.5s, ~1s, ~2s, …) with a little jitter so overlapping scripts don't sync-retry as a herd.
How to use this snippet
function retryUrlFetch(url, options, maxAttempts) {
maxAttempts = maxAttempts || 5;
options = options || {};
var lastError;
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
try {
var response = UrlFetchApp.fetch(url, options);
var code = response.getResponseCode();
// Treat rate limits and server errors as retryable when muteHttpExceptions is on
if (code === 429 || code >= 500) {
throw new Error('Retryable HTTP ' + code + ': ' + response.getContentText().substring(0, 200));
}
return response;
} catch (err) {
lastError = err;
if (attempt === maxAttempts) break;
var waitMs = Math.min(30000, Math.pow(2, attempt - 1) * 500 + Math.floor(Math.random() * 250));
Utilities.sleep(waitMs);
}
}
throw new Error('retryUrlFetch failed after ' + maxAttempts + ' attempts: ' + lastError);
}
Tips:
- Pass
{ muteHttpExceptions: true }inoptionsif you want HTTP 429/5xx to come back as responses you can inspect (the helper still retries those codes). - Don't retry non-idempotent POSTs blindly unless the API is safe to replay — prefer idempotency keys when the provider supports them.
- Cap attempts so a dead endpoint doesn't burn your whole execution budget.
Example
function fetchJsonWithRetry(url) {
var response = retryUrlFetch(url, {
method: 'get',
muteHttpExceptions: true,
headers: { Accept: 'application/json' }
}, 4);
return JSON.parse(response.getContentText());
}
Pair this with Script Properties for API keys (don't hardcode secrets) and you'll have a sturdier default for client integrations.
Happy Coding!
