Skip to main content

Exponential Backoff Retry Strategy

Programming Last updated:

It is a technique where an application progressively increases the waiting time between retry attempts after a failed operation

Table of Contents

TL;DR

  • Transient Failures do happen, for which we can retry with a limit
  • Retry increases load on already overloaded servers
  • Retry by adding a delay that increases exponentially, will reduce the load.

Sample Scenario

  • You have an UI, which shows some data.
  • The data is fetched at client side via some API
  • The server serving the request is overloaded or some throttling is implemented, hence the server is rejecting new calls.
  • Making the api call again may succeed.
  • So this application needs to add retries.
  • But then, if we add retry, would it not add load on an already overloaded system, increasing the failure rates?

Strategy 1 - Simple Retry

  • We want the users to see the data ASAP
  • So we will retry ASAP
type AsyncFunction<T> = () => Promise<T>
async function retryAsync<T>(fn: AsyncFunction<T>, retries: number): Promise<T> {
try {
return await fn()
} catch (error) {
if (retries > 0) {
console.log(`Retrying... attempts left: ${retries}`)
return retryAsync(fn, retries - 1)
} else {
throw error
}
}
}

Problem with Immediate Retry

  • The server on load will increase drastically, You would be calling this 1000/response-time-in-ms
  • Eg: For an API with response time of 50ms, it would now make 20 calls per second.
  • So we should probably add some waiting time before the next call

Strategy 2: Retry with constant wait

function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async function retryAsyncWithWait<T>(fn: () => Promise<T>, retries: number, delay: number): Promise<T> {
try {
return await fn()
} catch (error) {
if (retries > 0) {
console.log(`Retrying... attempts left: ${retries}`)
await wait(delay)
return retryAsyncWithWait(fn, retries - 1, delay)
} else {
throw error
}
}
}

Strategy 3: Retry with exponential Wait

function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
export async function exponentialBackoffRetryRecursive<T>(
asyncFunction: () => Promise<T>,
retries: number,
delay: number,
attempt: number = 0
): Promise<T> {
try {
return await asyncFunction()
} catch (error) {
if (attempt >= retries) {
throw error
}
await wait(delay * Math.pow(2, attempt))
return exponentialBackoffRetryRecursive(asyncFunction, retries, delay, attempt + 1)
}
}

Final Code: Abortable Retry with Exponential Back-off

The Functions should be abortable in production code.

import { waitAbortable } from './wait-abortable'
export async function exponentialBackoffRetryRecursive<T>(
asyncFunction: () => Promise<T>,
retries: number,
delay: number,
attempt: number = 0,
signal?: AbortSignal
): Promise<T> {
if (signal?.aborted) {
throw new Error('Operation aborted')
}
try {
return await asyncFunction()
} catch (error) {
if (attempt >= retries) {
throw error
}
await waitAbortable(delay * Math.pow(2, attempt), signal)
return exponentialBackoffRetryRecursive(asyncFunction, retries, delay, attempt + 1, signal)
}
}
export function waitAbortable(ms: number, signal?: AbortSignal): Promise<void> {
const { promise, reject, resolve } = Promise.withResolvers<void>()
if (signal?.aborted) {
reject(new Error('Operation aborted'))
}
const timeoutId = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => {
clearTimeout(timeoutId)
reject(new Error('Operation aborted'))
})
return promise
}

References

Tags