A tool returns 429. The agent retries immediately, gets another 429, retries again - and a transient blip becomes a self-inflicted outage, with every concurrent agent hammering in lockstep. Exponential backoff spaces the retries out; jitter breaks the lockstep. Before shipping either, you want to know the actual numbers: how long the worst case takes, and when the growth flattens against the cap.
For a 0-based attempt index i, the nominal delay is
di=min(cap, base⋅factori)
Jitter turns each fixed delay into a range. With a jitter fraction j∈[0,1] the actual sleep is drawn uniformly from
[di(1−j), di]
so j=0 is no jitter, j=0.5 is "equal jitter" and j=1 is "full jitter" (the range starts at zero). You are computing the deterministic bounds, not sampling - the schedule must be reproducible.
Implement:
def backoff_schedule(base, factor, cap, attempts, jitter):
Return a dict:
All floats are rounded to 4 decimal places. capped_at is an int. With attempts = 0, delays is empty and both totals are 0.0.
print(backoff_schedule(1.0, 2.0, 8.0, 5, 0.5)["delays"])
# [[0.5, 1.0], [1.0, 2.0], [2.0, 4.0], [4.0, 8.0], [4.0, 8.0]]
Delays double until they hit the 8.0 cap at attempt 3, and equal jitter puts the lower bound at half of each.
print(backoff_schedule(1.0, 2.0, 8.0, 5, 0.5)['delays'])
[[0.5, 1.0], [1.0, 2.0], [2.0, 4.0], [4.0, 8.0], [4.0, 8.0]]
Uncapped delays are 1, 2, 4, 8, 16; the cap of 8.0 clamps attempts 3 and 4 to 8.0. Equal jitter (j = 0.5) sets each lower bound to d * (1 - 0.5) = d / 2.
base > 0, factor >= 1.0, cap > 0, 0 <= attempts <= 64, 0 <= jitter <= 1base * factor**0 = basecapped_at compares the uncapped delay against cap using >=