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}