v9.1.1
Menu

Exponential Backoff Schedule with Jitter Bounds

easyFREEReliability & Failure HandlingAI AgentsAI Agents 1

Problem Statement

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.

Background

For a 0-based attempt index ii, the nominal delay is

di=min⁡(cap, base⋅factor i)d_i = \min(\text{cap},\ \text{base} \cdot \text{factor}^{\,i})

Jitter turns each fixed delay into a range. With a jitter fraction j∈[0,1]j \in [0, 1] the actual sleep is drawn uniformly from

[ di(1−j), di ][\,d_i (1 - j),\ d_i\,]

so j=0j = 0 is no jitter, j=0.5j = 0.5 is "equal jitter" and j=1j = 1 is "full jitter" (the range starts at zero). You are computing the deterministic bounds, not sampling - the schedule must be reproducible.

Your Task

Implement:

def backoff_schedule(base, factor, cap, attempts, jitter):

Return a dict:

  • delays: list of [lo, hi] pairs, one per attempt, each rounded to 4 decimal places.
  • best_case_total: sum of the (already rounded) lo values, rounded to 4 dp.
  • worst_case_total: sum of the (already rounded) hi values, rounded to 4 dp.
  • capped_at: the smallest 0-based attempt index whose uncapped delay base * factori** is >= cap, or -1 if the cap is never reached within attempts.

Input/Output Format

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.

Sample

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.

Example

Input
print(backoff_schedule(1.0, 2.0, 8.0, 5, 0.5)['delays'])
Output
[[0.5, 1.0], [1.0, 2.0], [2.0, 4.0], [4.0, 8.0], [4.0, 8.0]]
Reasoning
  • Inputs: base =1.0= 1.0 s is the first delay, factor =2.0= 2.0 doubles it each attempt, cap =8.0= 8.0 s is the ceiling, attempts =5= 5 asks for five delays (indices i=0i = 0 to 44), and jitter =0.5= 0.5 is equal jitter.
  • Uncapped growth: base⋅factori\text{base} \cdot \text{factor}^i gives 1,2,4,8,161, 2, 4, 8, 16 seconds. Attempt 33 is the first where this reaches the cap (8≥88 \ge 8), so the capped-at index is 33.
  • Apply the cap: di=min⁡(8,⋅)d_i = \min(8, \cdot) leaves 1,2,4,81, 2, 4, 8 alone and pulls 1616 down to 88. These are the upper bounds of each range.
  • Lower bounds: di(1−0.5)=di/2d_i(1 - 0.5) = d_i / 2 gives 0.5,1,2,4,40.5, 1, 2, 4, 4. Pairing each lower bound with its did_i yields [[0.5, 1.0], [1.0, 2.0], [2.0, 4.0], [4.0, 8.0], [4.0, 8.0]].
  • Totals: best case 0.5+1+2+4+4=11.50.5 + 1 + 2 + 4 + 4 = 11.5 s, worst case 1+2+4+8+8=23.01 + 2 + 4 + 8 + 8 = 23.0 s.
  • Notice the last two pairs are identical: once the cap bites, extra attempts add a flat 88 s each instead of doubling. Full jitter would make every lower bound 00.

Constraints

  • base > 0, factor >= 1.0, cap > 0, 0 <= attempts <= 64, 0 <= jitter <= 1
  • Attempt indices are 0-based: the first delay is base * factor**0 = base
  • Every delay bound is rounded to 4 decimal places
  • Totals are the sums of the already-rounded bounds, rounded again to 4 dp
  • capped_at compares the uncapped delay against cap using >=
  • Never emit a raw unrounded float

Learn this in

More from AI Agents 1

solution.py

Test results

Sign in to run your code against the test cases.