Have you ever implemented an integration with a third-party service where you have to call their API endpoints several times a day? Depending on the number of times you call this API, some of those calls will inevitably fail.
One solution to mitigate this problem is to implement a retry algorithm. Here
is a sequence diagram showing how this algorithm could look like:
Notice that once our API call fails, our app immediately tries to call it again. That could be extremely fast and there is nothing wrong with that, but that isn’t very effective. Why?
Being polite with Exponential Backoff
Lets assume the restaurants API we were trying to call on the diagram above is having some trouble. Maybe it’s overloaded or is completely down. Retrying to call it immediately after a failed attempt will do no good. It will actually make the situation worse: The restaurants API will be hammered harder and won’t have time to recover.
To countermeasure that, we can wait a little before retries. We can actually do better than that. What if on every failed attempt, we exponentially increase the waiting time for the next attempt? Bingo, This is what Exponential Backoff is.
- Our app tries to call the Restaurants API.
- The API call fails.
- Our app waits for
200 millisecodsbefore calling it again.- Our app retries to call the Restaurants API again.
- The API call fails again.
- Our app waits for
400 millisecodsbefore calling it again.- Our app retries to call the Restaurants API again.
- The API call completes successfully.
Here is how the diagram would look like when we implement Exponential Backoff:
How can we do that in Javascript?
The implementation of the algorithm above is actually quite straightforward in Javascript. The implementation below works in Node.js and also in modern browsers, with zero dependencies.
1/**2 * Wait for the given milliseconds3 * @param {number} milliseconds The given time to wait4 * @returns {Promise} A fulfilled promise after the given time has passed5 */6function waitFor(milliseconds) {7 return new Promise((resolve) => setTimeout(resolve, milliseconds));8}9 10/**11 * Execute a promise and retry with exponential backoff12 * based on the maximum retry attempts it can perform13 * @param {Promise} promise promise to be executed14 * @param {function} onRetry callback executed on every retry15 * @param {number} maxRetries The maximum number of retries to be attempted16 * @returns {Promise} The result of the given promise passed in17 */18function retry(promise, onRetry, maxRetries) {19 // Notice that we declare an inner function here20 // so we can encapsulate the retries and don't expose21 // it to the caller. This is also a recursive function22 async function retryWithBackoff(retries) {23 try {24 // Make sure we don't wait on the first attempt25 if (retries > 0) {26 // Here is where the magic happens.27 // on every retry, we exponentially increase the time to wait.28 // Here is how it looks for a `maxRetries` = 429 // (2 ** 1) * 100 = 200 ms30 // (2 ** 2) * 100 = 400 ms31 // (2 ** 3) * 100 = 800 ms32 const timeToWait = 2 ** retries * 100;33 console.log(`waiting for ${timeToWait}ms...`);34 await waitFor(timeToWait);35 }36 return await promise();37 } catch (e) {38 // only retry if we didn't reach the limit39 // otherwise, let the caller handle the error40 if (retries < maxRetries) {41 onRetry();42 return retryWithBackoff(retries + 1);43 } else {44 console.warn("Max retries reached. Bubbling the error up");45 throw e;46 }47 }48 }49 50 return retryWithBackoff(0);51}And here is how you can quickly test this implementation:
1/** Fake an API Call that fails for the first 3 attempts2 * and resolves on its fourth attempt.3 */4function generateFailableAPICall() {5 let counter = 0;6 return function () {7 if (counter < 3) {8 counter++;9 return Promise.reject(new Error("Simulated error"));10 } else {11 return Promise.resolve({ status: "ok" });12 }13 };14}15 16/*** Testing our Retry with Exponential Backoff */17async function test() {18 const apiCall = generateFailableAPICall();19 const result = await retry(20 apiCall,21 () => {22 console.log("onRetry called...");23 },24 425 );26 27 assert(result.status === "ok");28}29 30test();If you want to try this out, here is a Codesanbox link where you can play with it.