RxJS: retry with exponential backoff
Mateusz Garbaciak
Posted on April 17, 2023
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));
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 }));
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);
}
})
);
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.
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:
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:
Posted on April 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.