DEV Community

Cover image for RxJS: retry with exponential backoff
Mateusz Garbaciak for This is Learning

Posted on • Edited on

RxJS: retry with exponential backoff

Cover photo by Lucas Andrade on Pexels

Http request may occasionally fail. RxJS provides us with a retry operator that helps us make a call again in the case of a failure. Let's see how to implement retries with exponential backoff, i.e. delay that grows with every request.

Back in the days, exponential backoff could be implemented using zip operator to keep track of an index, or saving error index in closure. Currently it's possible to implement it in a simpler way.

Let's first have a look at function signature.

Type of configOrCount is a union of number or RetryConfig.

Simple retry

If we provide a number argument for a retry, it stands for maximum count of retries after error. The example below will retry for next 3 times, without a delay.

source$.pipe(retry(3));
Enter fullscreen mode Exit fullscreen mode

Retry with a delay

If we want to add some delay, we need to provide a retry config object. Below there is an Observable that is going to be retried 3 times, each attempt with a 200ms delay.

source$.pipe(retry({ count: 3, delay: 200 }));
Enter fullscreen mode Exit fullscreen mode

Retry with backoff

Exponential backoff is about increasing the delay with every failure, to reduce the rate of HTTP queries to server. We might want to make the first retry after 200ms, then after 400ms, third failed attempt should happen after 800ms and so on.

A delay can be either a number, as we have just seen that in the previous example, or a function returning an observable. We will concentrate on the latter now.

source$.pipe(
  retry({
    count: 3,
    delay: (_error, retryIndex) => {
      const interval = 200;
      const delay = Math.pow(2, retryIndex - 1) * interval;
      return timer(delay);
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

In configuration object we define maximum number of retries, setting it's count to 3, same as in previous example, what changed is a delay property, which is now a function. It accepts two parameters, error and current index of a retry. We are ignoring first one and using only retryIndex to use it when calculating an exponent.

Our expected backoff values should look like the following:

attempt | value [ms]
1       | 200
2       | 400
3       | 800

Enter fullscreen mode Exit fullscreen mode

To calculate the dalay we are going to use this formula:

delay = 2^(retryIndex-1) * interval

// results:
2^0*200 = 200
2^1*200 = 400
2^2*200 = 800
Enter fullscreen mode Exit fullscreen mode

Once we have calculated the number of milliseconds to wait, last thing is to use timer function from RxJS. It is going to do the actual wait, as it emits a value (0) after a specific amount of time.

Extract it to RxJS operator

To make this piece of code more reusable, we can extract it to another function.

const CONFIG = { count: 3, delay: 200 };

export function backoffRetry<T>({ count, delay } = CONFIG) {
  return (obs$: Observable<T>) => obs$.pipe(
    retry({
      count,
      delay: (_, retryIndex) => {
        const d = Math.pow(2, retryIndex - 1) * delay;
        return timer(d);
      }
    })
  );
}
Enter fullscreen mode Exit fullscreen mode

which could be later used as follows:

source$.pipe(backoffRetry());
Enter fullscreen mode Exit fullscreen mode

Summary

We had a look at how to do retries with RxJS, from simplest form to our desired backoff solution. Using this technique you can decrease excessive load on server on error requests.

References:

Top comments (0)