Engrain by ByteRush

JS.11.06 Mid-level· 12 minCore — asked often

Retry, timeout and backoff: failing well when the network does not

Retry only safe operations, wait longer each time with exponential backoff and jitter, respect Retry-After, and use timeouts that really stop the work.

Comes up in: Coding, Backend design

Learn#

Leela’s payment page retried a failed request three times, at once. The server had been slow, not down, so the customer was charged twice and the server got even busier.

A retry helps only with temporary failures, such as a timeout, a 503 or a dropped connection. Retry only idempotent operations: running them twice has the same effect as once. A GET is; a payment is not, unless it carries an idempotency key.

Exponential backoff doubles the wait each time: 500 ms, 1 s, 2 s, 4 s. Jitter picks a random time below that cap, so thousands of clients do not retry at the same moment.

Four attempts with backoff, jitter and Retry-AfterStep 1 of 4

Network

  • GET /orders → 503

State

  • attempt = 1
  • wait = random(0–500) = 310 ms

Attempt 1 fails with 503. The cap for the first wait is 500 ms; jitter picks a random time below it, here 310 ms.

Read the steps as text
  1. Attempt 1 fails with 503. The cap for the first wait is 500 ms; jitter picks a random time below it, here 310 ms.
  2. Attempt 2 fails. The cap doubles to 1,000 ms; jitter picks 720 ms.
  3. Attempt 3 gets 429 with Retry-After: 2. The server asked for 2 seconds, so we wait 2,000 ms.
  4. Attempt 4 succeeds. If it had failed again at the attempt limit, we would stop and show the error.

A timeout built with Promise.race gives up waiting, but the slow work keeps running:

let finished = false;
const slowSave = new Promise((r) => setTimeout(() => { finished = true; r("saved"); }, 100));
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), 50));
try { await Promise.race([slowSave, timeout]); }
catch (e) { console.log(e.message, "| finished:", finished); }
await new Promise((r) => setTimeout(r, 100));
console.log("later, finished:", finished);

Output (verified)

timed out | finished: false
later, finished: true

Predict#

Predict first

All 10,000 clients get a 503 at the same second and retry with backoff but no jitter. What happens?

Interview angle#

Say it like this — about 30 seconds

“I retry only idempotent requests, or ones with an idempotency key, and only on temporary errors such as timeouts, 429 and 5xx, never on a 400. Each wait is a random time up to base times two to the attempt, capped, so clients do not retry together. I stop after a few attempts and respect Retry-After. Timeouts use an abort signal, because Promise.race leaves the slow request running. Under a long outage, a circuit breaker stops calls for a while.”

Stays on your device. Nothing is uploaded.
Follow-up If Promise.race is used for a timeout, what happens to the slower promise?
It keeps running, and its result or error is ignored. The request still uses the network and the server.
Follow-up Which errors should you not retry?
Client errors such as 400, 401, 403 and 404: the same request will fail the same way.
Follow-up What is a circuit breaker?
After many failures, it stops calling a service for a cool-down time and fails fast, then lets a test call through.

Practice#

Timed, like the real round

Implement retryWithBackoff with full jitter, an attempt limit and Retry-After support.

Done when

  • It stops after retries + 1 calls and rejects with the last error.
  • Waits follow random() * min(max, base * 2 ** (n - 1)).
  • error.retryAfterMs overrides the computed wait.
  • shouldRetry(error) === false stops at once.
Your solution
Ctrl/⌘ + Enter

Remember#

Try to answer each card before you open it. Then add them to your reviews.

What does idempotent mean?

Doing it twice has the same effect as doing it once.

Why add jitter to backoff?

So many clients do not retry at the same moment.

What happens to the slow promise when Promise.race times out?

It keeps running; only its result is ignored.

Which status codes are usually worth retrying?

429, 503 and other temporary 5xx errors, and timeouts.

Go deeper

Four layers. Stop at the one your interview needs; each ends with a quick check.

The core ideaEvery round

Always set a limit: three to five attempts for user-facing calls. After that, show a clear error with a “Try again” button.
The cap for retry n is base * 2 ** (n - 1), limited by a maximum.

Check yourself: What is the wait cap for retry 4 with base 500 ms and no max?

500 × 2³ = 4,000 ms.

How it works underneathSenior rounds

“Full jitter” waits a random time between zero and the cap. AWS found it spreads load well while keeping total work low Source: AWS Architecture Blog: Exponential backoff and jitter.

Check yourself: In full jitter, what is the wait for a cap of 1,000 ms?

A random value between 0 and 1,000 ms.

Edge cases and trapsWhere people slip

A 429 or 503 may carry Retry-After: seconds, or an HTTP date. Use it instead of your own delay Source: RFC 9110: Retry-After.
Give each attempt its own timeout through an abort signal, and pass the user’s signal too, so leaving the page stops the retries.

Check yourself: Retry-After can hold two kinds of value. Which?

A number of seconds, or an HTTP date.

In productionStaff-level answers

A circuit breaker counts failures. When too many happen, it “opens” and fails calls at once for a cool-down time. Then it lets one test call through, and closes again if that succeeds.
Retry in one layer only: retries in the client, the gateway and the service multiply.

Check yourself: What does a circuit breaker do in the open state?

Fails calls at once without calling the service, until a cool-down ends.

Finished the five steps? Mark it, and its cards join your reviews.

Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

Interview questions this answers

  • Implement retry with exponential backoff.
  • If Promise.race is used for a timeout, what happens to the slower promise?