Retry-with-backoff policy
Implement the pure function that turns a sequence of API replies into the exponential-backoff wait schedule, retrying only what a retry can fix.
On this page
A client that retries the wrong things, or retries the right things at a fixed interval, turns a vendor's bad minute into your bad hour. The policy is a pure function, and this exercise writes it.
The situation
During a vendor incident a team's agent client retried every failure five times at a fixed one-second interval, including a batch of 400s caused by a tool schema that had failed validation since the morning deploy. Two hundred workers retried in lockstep, the 529s kept coming, and the 400s were retried 1,000 times before anyone read a response body. The incident review asked for one function that decides the wait schedule, testable without a network.
The contract: given the reply each attempt will receive, return the waits in milliseconds before each retry. Retry 408, 429 and any 5xx; never retry any other 4xx; stop on a 2xx. The k-th retry waits min(cap_ms, base_ms × 2^(k−1)), raised to the server's retry-after when one was sent. At most max_attempts attempts in total. Jitter is applied by the caller, on top of any retry-after, which is a floor — the function returns the deterministic wait, which is what makes it testable, as retries, timeouts and rate limits argues.
The question
Implement the schedule so that the four examples below hold, with the 400 branch producing no retry and the attempt cap ending the schedule without a trailing wait.
def solve(statuses, base_ms, cap_ms, max_attempts):
"""statuses: the reply each attempt will receive, in order — an int status
code, or a (status, retry_after_ms) tuple when the server sent Retry-After.
Returns the list of waits in ms before each retry. Retry 408, 429 and 5xx;
never retry any other 4xx; stop on 2xx. The k-th retry waits
min(cap_ms, base_ms * 2 ** (k - 1)), raised to retry_after_ms when the
server sent one. At most max_attempts attempts in total. Jitter is the
caller's job, so the schedule is deterministic."""
solve([500, 500, 200], 100, 10000, 5) -> [100, 200]
solve([429, 429, 429, 429, 200], 100, 400, 5) -> [100, 200, 400, 400]
solve([400, 200], 100, 10000, 5) -> []
solve([(429, 1500), 200], 100, 10000, 3) -> [1500]
A strong answer · Rubric · Where answers go wrong · Solution
Write your answer first, then open