Skip to content
Safe tool use · 2 of 2 · about 20 minutes

Bound retry delays

Calls to a model or a tool sometimes fail for a moment: the service is busy, or the network drops. Trying again often works. But trying again at once, and for ever, makes a busy service busier. Exponential backoff is the polite way to retry: wait a little, then twice as long, then twice as long again. Two limits keep it sane. A cap stops the wait from growing without end, and a retry budget says when to give up.

Learn it

Words to know

retry
Trying a failed call again, in the hope that the failure was brief.
exponential backoff
Waiting longer before each retry, usually twice as long as the time before.
retry budget
The most retries allowed before the caller gives up and reports the failure.

How it works

  1. Attempts are numbered from 0. If attempt is maxRetries or more, the budget is used up: return -1, which means give up.
  2. Otherwise the wait doubles with every attempt: baseMs * 2 ** attempt. For a base of 100 that is 100, 200, 400, 800.
  3. Never wait longer than capMs: return the smaller of the two.

Worked example

Input
attempt = 3
baseMs = 100
capMs = 500
maxRetries = 5

Output
500
AttemptDoubled waitAfter the cap of 500Answer
0100100100
1200200200
2400400400
3800500500
41600500500
5not computednot computed-1: give up
wait in ms04008001200160001234attemptattempt 3: 500attempt 3: 500
  • Doubling only
  • With the cap
A base of 100 milliseconds and a cap of 500. Without the cap the wait keeps doubling. With it the wait flattens at 500, and after attempt 4 the budget of 5 retries is used up.

The example asks for attempt 3: the doubled wait of 800 is over the cap, so the answer is 500. The other rows show the same settings for attempts 0, 1, 2, 4 and 5.

Why it matters

Every call to a model service needs this, and client libraries often do it for you. Real retry code adds a small random change to each wait, called jitter, and takes care not to repeat a write that succeeded.

Your turn

Return how long to wait before a retry, or -1 when it is time to give up.

attempt
which retry this is, counted from 0; a whole number
baseMs
the wait before the first retry, in milliseconds; a whole number of 1 or more
capMs
the longest wait allowed, in milliseconds
maxRetries
how many retries are allowed in total; with 3, attempts 0, 1 and 2 are allowed
Returns
The wait in milliseconds: baseMs doubled attempt times, but at most capMs. If attempt is maxRetries or more, -1.

What JavaScript gives you

2 ** n
2 to the power n
Math.min(a, b)
the smaller of two numbers

What Python gives you

2 ** n
2 to the power n
min(a, b)
the smaller of two numbers

Example 1

Input
attempt = 0
baseMs = 100
capMs = 1000
maxRetries = 3

Output
100

The first retry waits the base time: 100 doubled zero times.

Example 2

Input
attempt = 3
baseMs = 100
capMs = 500
maxRetries = 5

Output
500

100 doubled three times is 800, which is over the cap of 500.

Example 3

Input
attempt = 3
baseMs = 100
capMs = 1000
maxRetries = 3

Output
-1

With 3 retries allowed, the attempts are 0, 1 and 2. Attempt 3 is one too many.

Example 4

Input
attempt = 2
baseMs = 100
capMs = 1000
maxRetries = 3

Output
400

The last allowed attempt: 100 doubled twice.

The checks also try

  • no retries allowed at all
  • a cap below the base
  • the cap cuts the third wait
  • ten doublings
  • one attempt later, still at the cap
  • the first attempt past the budget
  • an attempt well past the budget
  • forty doublings, far over the cap

Hints

Each hint gives away a little more. Open one only when you need it.

  1. Hint 1: Where to start

    Two checks, in order: is there any budget left, and is the doubled wait above the cap?

  2. Hint 2: The key step

    The wait before the cap is the base multiplied by 2 to the power of the attempt. Attempt 0 gives the base itself, because 2 to the power 0 is 1.

  3. Hint 3: The outline

    If attempt is at least maxRetries, return -1. Otherwise compute baseMs times 2 to the power attempt, and return that or capMs, whichever is smaller.

Solution

From here on the page gives the method away. If you are still solving, go back to the hints.

Step by step

  1. Out of retries: give up. Attempts count from 0, so with 3 retries allowed the last valid attempt is 2. The test is at least, not greater than.
  2. The wait doubles with every attempt. Doubling backs off quickly. After a few failures the caller is waiting long enough for a busy service to recover.
  3. Never wait longer than the cap. Without a cap, attempt 20 with a base of 100 milliseconds would wait more than a day.

The same numbered steps are the comments in the solution.

JavaScript
function solve(attempt, baseMs, capMs, maxRetries) {
  // 1. Out of retries: give up
  if (attempt >= maxRetries) return -1;
  // 2. The wait doubles with every attempt
  const wait = baseMs * 2 ** attempt;
  // 3. Never wait longer than the cap
  return Math.min(capMs, wait);
}
Python
def solve(attempt, baseMs, capMs, maxRetries):
    # 1. Out of retries: give up
    if attempt >= maxRetries:
        return -1
    # 2. The wait doubles with every attempt
    wait = baseMs * 2**attempt
    # 3. Never wait longer than the cap
    return min(capMs, wait)

Watch out

  • maxRetries is a count, not the last attempt's number. With 3 retries, attempt 3 is already one too many.
  • Apply the cap to the first attempt too. A base of 500 with a cap of 100 waits 100.
  • Only retry failures that can pass. A timeout may succeed next time; a wrong password or an invalid request fails the same way every time.

Check yourself

Why does the wait grow instead of staying the same?

A service that is failing is often overloaded. Waiting longer each time gives it room to recover instead of adding to the load.

What are the cap and the retry budget for?

The cap keeps a single wait from growing without end. The budget ends the retrying, so the caller can report a failure.