HTTP Fetch with Retry and Backoff
A Node module that wraps fetch with bounded retries, exponential backoff, jitter, and retryable-status detection, returning the final response or throwing.
Project
node-http-retry
Entry point: retry.mjs
Install
node --versionRun
node retry.mjs https://example.comTest
node --test retry.test.mjsExpected output
Prints how many attempts were made and the final status code of the request.Dependencies
- node >=20
What it does
Wraps fetch with the retry policy you would otherwise write inline every time: retry only transient failures, back off exponentially, and give up after a cap.
export async function fetchWithRetry(url, {
attempts = 3,
baseDelay = 100,
fetchImpl = fetch,
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
shouldRetry = (res, err) => err != null || (res && res.status >= 500),
} = {}) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
const res = await fetchImpl(url);
if (!shouldRetry(res, null) || attempt === attempts) return { res, attempts: attempt };
lastError = new Error('retryable status ' + res.status);
} catch (err) {
if (attempt === attempts) throw err;
lastError = err;
}
await sleep(baseDelay * 2 ** (attempt - 1));
}
throw lastError;
}
const url = process.argv[2];
if (!url) { console.error('usage: retry <url>'); process.exit(1); }
const { res, attempts } = await fetchWithRetry(url);
console.log('attempts=' + attempts + ' status=' + res.status);
The retry predicate is injected so the same wrapper works for status-based, error-based, or custom retry rules.
Run and verify
node retry.mjs https://example.com
Run against a reachable URL; a temporary 5xx response is retried while a 404 returns immediately.
Expected Output
For a healthy endpoint on the first try the program should report one attempt and the status. The output is:
attempts=1 status=200
A retryable failure increments the attempt count up to the cap, and a non-retryable status such as 404 is returned on the first attempt.
Limitations
Backoff is exponential with no jitter by default; add randomisation when many clients retry the same endpoint to avoid synchronised spikes.