Take the exponential ceiling, keep half of it as a floor, and draw the rest:
half = min(cap, base * 2^(attempt-1)) / 2
delay = half + rng.nextInt(half + 1)
Every delay lands in [half, 2 x half] and averages about three quarters of the
un-jittered ceiling, against full jitter’s one
half and exponential’s whole.
On the canonical workload it sits exactly where that arithmetic predicts: 30.2% success against exponential’s 40.0% and full jitter’s 19.9%, with a herd of 3.1% against 15.1% and 2.6%. It buys back most of exponential’s success rate while keeping nearly all of full jitter’s spreading.
When to use it
- When the backoff must actually back off. Full jitter can return zero on any attempt, so an unlucky client retries immediately eight times running and gives a struggling service no respite. This cannot fall below half the ceiling, ever.
- In front of something that needs recovery time. A database replaying a log, a cache warming, a connection pool refilling: a guaranteed minimum matters more here than the last few percent of dispersal.
- Against a rate limiter you must not trip again immediately. A zero-length
wait after a
429is wasted work at best. - When you want jitter but the zero delays make reviewers nervous. That is a legitimate reason: a policy nobody trusts gets configured around.
When not to use it
- When maximum dispersal is the goal. Full jitter spreads over twice the range, and decorrelated jitter spreads whole schedules. This one’s clients are all within a factor of two of each other on every attempt.
- With a very small base. Integer halving means a ceiling of 1 halves to 0 and every delay becomes 0: the policy stops backing off entirely. A vector pins that so nobody discovers it in production, but the real answer is not to configure a 1 ms base.
- When you have measurements saying full jitter is better. AWS’s 2015 numbers put full jitter slightly ahead on both completion time and total work, and this registry’s benchmark agrees on the herd metric. Equal jitter is the answer when the guaranteed floor is worth those points.
How it works
The halving is integer division, matching the three ports exactly. At a
ceiling of 1 the half is 0 and 0 + nextInt(1) is always 0: degenerate, and
documented rather than special-cased, because a special case would be a
different policy.
Both ends of the range are reachable. half + nextInt(half + 1) produces
half at one end and 2 x half at the other. A test checks both occur, since
an off-by-one in the bound would silently narrow the range.
Giving up consumes no randomness. The two refusal checks precede the draw, so a declined call leaves the stream where it was. A port that ordered these differently would diverge from the first give-up onward.
The exponential ceiling is restated here rather than imported from
exponential, because policybook add copies a policy file
whole and a copy that reached back into the registry would not compile in your
project. backoff-policies.test.ts pins the copies against each other.
Parameters
Complexity
O(attempt) time: a bounded loop that stops at the cap, so a handful of multiplications. O(1) space.
Source
Marc Brooker, Exponential Backoff And Jitter, AWS Architecture Blog, 2015,
where it appears alongside full jitter and decorrelated jitter as
sleep = temp/2 + random_between(0, temp/2).
Its neighbours bracket it precisely. exponential is this policy with the random half removed, and full jitter is this policy with the fixed half removed. The benchmark’s success rates of 40.0%, 30.2%, and 19.9% fall in exactly that order, which is a pleasant confirmation that the three are doing what the arithmetic says.
Notes
No patents known.
The Rng is supplied at construction rather than passed to nextDelay (see the
domain interface for why).