Retry a Failed Request With Backoff#

Networks fail, services have bad minutes, and rate limits bite. Instead of failing the whole run, retry a few times with a growing delay. This is the difference between a brittle job and a reliable one.

The Script#

// retry.js
async function fetchWithRetry(url, attempts = 4) {
	let delay = 1000;
	let lastError = null;

	for (let attempt = 1; attempt <= attempts; attempt++) {
		try {
			const res = await fetch(url);

			if (res.ok) {
				return res;
			}

			// 4xx responses (except 429) will not fix themselves.
			if (res.status < 500 && res.status !== 429) {
				const error = new Error(`Request failed: ${res.status}`);
				error.retryable = false;
				throw error;
			}

			lastError = new Error(`Request failed: ${res.status}`);
		} catch (error) {
			if (error.retryable === false || attempt === attempts) {
				throw error;
			}
			lastError = error;
		}

		if (attempt === attempts) {
			throw lastError;
		}

		console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
		await new Promise((resolve) => setTimeout(resolve, delay));
		delay *= 2;
	}
}

const res = await fetchWithRetry("https://api.github.com/zen");
console.log(await res.text());

How It Works#

  • The loop retries up to attempts times. The delay starts at one second and doubles each time (backoff), giving a struggling service room to recover.
  • Client errors such as 404 are not retried, since the same request will fail the same way. Rate limits (429) and server errors (5xx) are.
  • The last attempt rethrows, so a genuinely broken call still fails the run and shows up as an error.

Setup#

Keep the total time under the run time limit; with four attempts and backoff, this stays well within it.