mediumEngineering Practices#149

Exponential backoff with jitter

Prompt

Design the retry delays for a flaky API:

  1. backoffSchedule(retries, base, cap) → array of delays: base × 2^attempt, each capped at cap e.g. backoffSchedule(5, 100, 1000)[100, 200, 400, 800, 1000]
  2. withJitter(schedule, rand) → full jitter: each delay becomes rand() × delay (rand injected for testability; use Math.floor on the result)

Solution

function backoffSchedule(retries, base, cap) {
  return Array.from({ length: retries }, (_, attempt) =>
    Math.min(base * 2 ** attempt, cap)
  )
}

function withJitter(schedule, rand) {
  return schedule.map(delay => Math.floor(rand() * delay))
}
Mentor's take

Retry logic is where mobile engineers ship distributed-systems bugs without noticing. The interview question isn't "can you multiply by two" — it's whether you know why each of the three ingredients exists, because each one prevents a specific production incident.

  1. Exponential growth exists because the failure you're retrying against is usually load. Fixed-interval retries (every 2s, forever) hold constant pressure on a struggling server — backoff is the client voluntarily shedding load so the server can recover.
  2. The cap exists because 100ms doubled 10 times is 102 seconds — past a point, longer waits punish the user without helping the server. The cap is a product decision (how long will a user stare at a spinner?) wearing a technical costume.
  3. Jitter is the one candidates miss, and it's the senior differentiator: when a deploy drops 10,000 mobile clients at once, they all fail at t=0 and — without jitter — all retry at exactly t=100ms, t=300ms, t=700ms as a synchronized wave. That's a thundering herd: the retries are the outage. Full jitter (rand() × delay) spreads the wave into noise. AWS's architecture blog made "full jitter" the standard answer.
  • Injecting rand instead of calling Math.random() inline is the testability move — the tests below only pass because randomness is a parameter. Same reason you inject clocks.

The mobile-specific layer worth adding: retries are only safe on idempotent operations. Retrying a GET is free; retrying a POST /payments needs an idempotency key. And on device you cap total retry time, because the user can walk into a tunnel — reachability listeners beat blind persistence.

Say it: "Exponential backoff sheds load, the cap bounds user-visible latency, and jitter breaks retry synchronization — without jitter, ten thousand clients retrying in lockstep are the second outage."