Build the mental model
You already know a request eventually gets a response with a status code. What API Tutorial did not cover is what happens when it does not respond at all.
A hung connection, an overloaded server, or a dropped network packet can leave your code waiting indefinitely unless you set a client-side timeout: a hard limit after which you give up and treat the request as failed, even though the server might still be working on it.
Once a request fails, the next decision is whether to retry it, and blind retrying is a real mistake. A 4xx client error, like a malformed request body or an invalid resource ID, will fail again identically no matter how many times you resend it, because the problem is in what you sent, not in the network.
- Temporary server failures (5xx status codes)
- Network-level failures, like a dropped connection
- 429 rate-limit responses, especially when the provider's docs say retrying is expected
When you do retry, spacing the attempts matters. Exponential backoff doubles the wait between each attempt, so you are not hammering a struggling server every few milliseconds.
Jitter adds a small random offset to that delay, so that many clients retrying the same failure do not all retry at the exact same instant and cause a new wave of overload. Together, backoff and jitter turn a naive retry loop into one that behaves responsibly under real failure conditions.
- Timeout
- A hard time limit after which a client gives up on a request and treats it as failed, because the server or network did not respond in time.
- Backoff
- A strategy that increases the wait between retry attempts; in exponential backoff, the delay doubles with every attempt.
EXPONENTIAL BACKOFF SEQUENCE
----------------------------
ATTEMPT 1 (t=0s) FAIL
|
| wait 1s
v
ATTEMPT 2 (t=1s) FAIL
|
| wait 2s
v
ATTEMPT 3 (t=3s) FAIL
|
| wait 4s
v
ATTEMPT 4 (t=7s) SUCCESS
Delay doubles each time: 1s -> 2s -> 4s (exponential backoff)Connect it to a real scenario
The function below simulates a client retrying a request against a fixed, pre-scripted sequence of outcomes, `["fail", "fail", "fail", "success"]`, rather than making any real network call. That keeps the example fully deterministic.
For each attempt after the first, the delay doubles: 1000ms, then 2000ms, then 4000ms, following the classic `baseDelay * 2^(attempt - 2)` formula. The loop stops as soon as it records a `"success"` outcome.
In production code you would add jitter on top of this delay and would actually `await` a timer instead of just recording the number, but the retry-and-backoff logic itself is identical to what ships in real SDKs.
Read the output as a small case study: three failures in a row would sink a naive retry-every-100ms loop into overloading a struggling server, but this schedule spreads the same four attempts across roughly seven seconds of wall-clock time, giving the server real room to recover.
Try the working example
function simulateRetries(outcomes, baseDelayMs = 1000) {
const log = [];
for (let attempt = 1; attempt <= outcomes.length; attempt++) {
const outcome = outcomes[attempt - 1];
const delay = attempt === 1 ? 0 : baseDelayMs * Math.pow(2, attempt - 2);
log.push({ attempt, waitedMs: delay, outcome });
if (outcome === "success") break;
}
return log;
}
const outcomes = ["fail", "fail", "fail", "success"];
console.log(simulateRetries(outcomes));attempt 1: outcome=fail, waited=0ms
attempt 2: outcome=fail, waited=1000ms
attempt 3: outcome=fail, waited=2000ms
attempt 4: outcome=success, waited=4000ms5-minute try-it
Copy `simulateRetries` and change the outcome sequence to `["fail", "fail", "success"]`. Predict the two backoff delays before running it, then run it and check your prediction.
One important caution
Retrying a 4xx client error -- resending the same malformed request just fails the same way again
Skipping jitter, so many retrying clients synchronize and hit the server in a thundering herd
Retry-After header - MDN — API Integration & Webhooks