# Retrying HTTP Requests: Exponential Backoff, Jitter and Idempotency

Networks fail, servers restart, rate limits kick in. A client that retries handles all of that gracefully — and a client that retries badly turns a small outage into a big one, as thousands of clients hammer a struggling server in lockstep. Good retries come down to four decisions: **what** to retry, **how long** to wait, **how many** times, and whether retrying is **safe** at all.

## What to retry

| Outcome | Retry? | Why |
|---|---|---|
| Network error or timeout | Yes | The request may never have arrived |
| [408 Request Timeout](/http/408) | Yes | The server gave up waiting for the request |
| [429 Too Many Requests](/http/429) | Yes, after `Retry-After` | You're rate limited; waiting is the fix |
| [502](/http/502), [503](/http/503), [504](/http/504) | Yes, with backoff | Usually temporary infrastructure trouble |
| [500 Internal Server Error](/http/500) | Maybe | Sometimes transient, often a bug that will fail again |
| Other 4xx (400, 401, 403, 404, 422…) | No | The request itself is wrong; sending it again won't help |

## Is it safe to retry?

A retry is only safe if doing the operation twice has the same effect as doing it once. HTTP defines GET, HEAD, OPTIONS, PUT and DELETE as **idempotent**: repeating a `PUT /users/42` leaves the same result. POST and PATCH are not — retrying `POST /payments` after a timeout can charge a card twice, because a [504](/http/504) doesn't tell you whether the first attempt finished.

The standard fix is an **idempotency key**: the client generates a unique id per logical operation and sends it with every attempt.

```http
POST /payments HTTP/1.1
Idempotency-Key: 7f9c2ba4-e88f-4c5d-9a1e-3b8f6d2c4a10
```

The server stores the result under that key; a retry with the same key gets the stored result instead of a second charge. Stripe popularised the pattern, and an IETF draft standardises the `Idempotency-Key` header.

## How long to wait: exponential backoff with jitter

Retrying immediately just adds load at the worst moment. Exponential backoff doubles the wait after each failure — 0.5 s, 1 s, 2 s, 4 s — up to a cap. On its own, though, it keeps clients synchronised: everyone who failed together retries together. **Jitter** randomises each wait to spread them out. The simplest effective version, called *full jitter*, picks a random delay between zero and the exponential limit.

And if the server tells you when to come back — [`Retry-After`](/headers/retry-after) on a 429 or 503 — believe it.

## A complete example

```js
const RETRYABLE = new Set([408, 425, 429, 500, 502, 503, 504]);

async function fetchWithRetry(url, options = {}, { retries = 4, baseMs = 500, capMs = 10_000 } = {}) {
  for (let attempt = 0; ; attempt++) {
    let res;
    try {
      // A fresh 10-second timeout for each attempt
      res = await fetch(url, { ...options, signal: AbortSignal.timeout(10_000) });
      if (!RETRYABLE.has(res.status) || attempt === retries) return res;
    } catch (err) {
      if (attempt === retries) throw err; // network error or timeout, out of retries
    }
    await new Promise((resolve) => setTimeout(resolve, delayFor(res, attempt, baseMs, capMs)));
  }
}

function delayFor(res, attempt, baseMs, capMs) {
  const header = res?.headers.get("Retry-After");
  if (header) {
    const seconds = Number(header);
    const ms = Number.isNaN(seconds) ? Date.parse(header) - Date.now() : seconds * 1000;
    if (ms > 0) return ms;
  }
  // Full jitter: random wait between 0 and the exponential limit
  return Math.random() * Math.min(capMs, baseMs * 2 ** attempt);
}
```

A few details are doing real work here:

- `Retry-After` comes in two forms — seconds (`120`) or an HTTP date — and both are handled.
- Each attempt gets its own timeout, so one hung connection can't eat the whole retry budget. (This replaces any `signal` you passed in; combine them with `AbortSignal.any()` if you need both.)
- The last failed response is returned rather than thrown, so callers still see the real status.
- Only use this for POST if you also send an idempotency key.

## How many times

Three to five attempts covers almost all transient failures. More than that mostly prolongs an outage. Two system-level rules matter even more:

- **Retry at one layer only.** If the browser, your API gateway and your backend each retry three times, one failure becomes 27 requests to the database.
- **Stop when things are clearly broken.** A *circuit breaker* fails fast for a while after repeated errors, giving the failing service room to recover instead of burying it.

## Server side: make retries cheap

If you run the API, you can make every client's retry logic work better:

- Send `Retry-After` with 429 and 503 responses.
- Return 429 for rate limits, not 500 or 403, so clients know waiting will help.
- Accept idempotency keys on POST endpoints that create things or move money.

For what each status code means from the server's side, see the [HTTP status code reference](/http), and for telling gateway errors apart, [502 vs 503 vs 504](/blog/502-vs-503-vs-504).
