RxJS: retry with exponential backoff
Tags: programming, javascript, rxjs, tutorial
Published: 2023-04-17 09:22:52

Cover photo by [Lucas Andrade](https://www.pexels.com/pl-pl/@lucasandrade/) 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.

```typescript
source$.pipe(retry(3));
```

#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.

```typescript
source$.pipe(retry({ count: 3, delay: 200 }));
```

#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.

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

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

```
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
```
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.

```typescript
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);
      }
    })
  );
}
```
which could be later used as follows:

```typescript
source$.pipe(backoffRetry());
```

#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:
- https://rxjs.dev/api/index/function/retry
- https://rxjs.dev/api/index/interface/RetryConfig
- https://rxjs.dev/api/index/function/timer
- https://github.com/alex-okrushko/backoff-rxjs/blob/master/src/operators/retryBackoff.ts
- https://en.wikipedia.org/wiki/Exponential_backoff