Double the wait after every failure, up to a cap: min(cap, base * 2^(n-1)).
The textbook answer, and a genuine improvement on constant:
load on a struggling service falls off geometrically as an outage continues, so
the service gets quieter exactly when it most needs to.
It still synchronises clients, and that is the reason not to ship it. The delay is a pure function of the attempt number, so every client that failed at the same moment retries at the same moment, every time. On the canonical workload its peak simultaneous retries are about 15% of all retries, no better than constant backoff, despite a completely different delay curve.
Backing off exponentially converts a continuous herd into a periodic one. It does not disperse it. Dispersal needs randomness, which is exponential-full-jitter, one extra line.
When to use it
- When the delay sequence must be reproducible without a random source.
Embedded systems, audit trails, and tests that assert exact timings all
legitimately need
nextDelay(3)to be 400 every time. - With a single client, where there is no fleet to synchronise and the jitter would only cost patience.
- When you want the full backoff duration. Full jitter’s expected delay is half the ceiling. If you need the whole thing, this is the policy that waits it, though equal-jitter gets most of it while still spreading.
- As the thing you explain first. The curve is the part everyone already understands, and the jittered policies are all “this, plus a draw”.
When not to use it
- Anywhere many clients can fail together, which is most production systems. The synchronisation is the whole problem, and the gentle-looking curve does not address it.
- In front of a service recovering from overload. A periodic wall of requests is not much kinder than a continuous one. What a recovering service wants is a trickle.
- As a default. exponential-full-jitter is the same policy plus one line, and AWS’s published measurements found it both faster and cheaper under contention.
How it works
if not error.retryable: return null
if attempt >= maxAttempts: return null
return min(capMs, baseMs * 2^(attempt - 1))
The doubling is a bounded loop, not a shift. It multiplies by two and stops
as soon as the cap is reached, so the arithmetic can never run past the width of
an integer however large the attempt number grows: base << (attempt - 1)
would be nonsense at attempt 40, and a test asks for attempt 1,000,000 to make
the point.
The cap wins from attempt one if it is set below the base. Not a configuration anyone should write, but it behaves rather than surprises.
backoffCeiling is exported and shared with the jittered policies, which
differ only in what they do with the number it returns. Keeping it in one place
is what stops them drifting apart.
Tie-breaking. A non-retryable failure gives up regardless of the attempt budget.
Parameters
At the defaults the policy is patient for 12.7 seconds in total (100 + 200 + … + 6400). That number explains most of the benchmark: against outages of up to thirty seconds, most episodes end in failure for every policy here, and that is the honest result rather than a broken harness.
Complexity
O(attempt) time: a bounded loop that stops at the cap, so a handful of multiplications in practice. O(1) space.
Source
Folklore. Exponential backoff predates the systems literature that discusses it, appearing in Ethernet’s collision handling in the 1970s. The specific observation that it is not enough on its own is Marc Brooker’s Exponential Backoff And Jitter (AWS, 2015), which is cited by exponential-full-jitter.
Its neighbours are constant, which is this without the growth,
and exponential-full-jitter, which is this plus a
uniform draw. The three-way comparison on the canonical workload is pinned by
backoff-policies.test.ts, including the uncomfortable finding that full jitter
succeeds less often than this policy while still being the better default.
Notes
No patents known.
The Rng is supplied at construction rather than passed to nextDelay (see the
domain interface for why). This policy never draws from it, which is exactly why
it synchronises.