Opinionated modern exponential backoff retry driver
As of version 3.x
callback support is dropped.
npm i -S @kessler/exponential-backoff
const backoff = require('@kessler/exponential-backoff')
async function main() {
// if an error is thrown then work will be retried
// with an appropriate delay
// after `maxAttempts` is exceeded
// an 'operation failed, exceeded maximum attempts' Error
// is thrown and the retry process stops
const work = () => httpRequest('http://example.com')
const response = await backoff(work)
}
const backoff = require('@kessler/exponential-backoff')
async function main() {
const work = () => httpRequest('http://example.com')
// more details about these options below
const response = await backoff(work, {
maxAttempts: 100,
maxExponent: 10
})
}
- maxAttempts (Default 100) - maximum number of attempts before giving up. Meaning, if the 100th attempt fails an error will be thrown.
- throwMaxAttemptsError (Default true) - if set to false, no error will be thrown when maxAttempts is exceeded
- delayInterval (Default 100) - minimum delay unit, random(0, Math.pow(base, attemptNumber or maxAttempts)) * delayInterval = next delay
- base (Default 2) - base exponent for backoff algorithm
- maxExponent (Default 10) - in exponential backoff the number of attempts is used as the exponent, this is the maximum value that can be used even if retries exceed this value
- unrefTimer (Default false) - retry delay is achieved using setTimeout(). by default it will be unrefed, so the process will not wait for these timers to finish
You can also iterate over the attempts for better insight into and control over the process.
const backoff = require('@kessler/exponential-backoff')
async function main() {
const work = () => httpRequest('http://example.com')
const iterator = backoff.iterator(work /*, { you can provide options here } */)
for await (let attempt of iterator) {
console.log(`this is attempt #${attempt}`)
if (attempt > 0) {
console.log(iterator.lastError)
}
if (attempt > 2) {
break;
}
}
// proceed from attempt #2 until maxAttempts
for await (let attempt of iterator) {
}
console.log(iterator.result)
}
The startup code in backoff()
has a cost. You can see the code in bench.js. In situations where you need a performence optimization, use the cached version (but remember: preoptimization is the root of all evil!):
const backoff = require('@kessler/exponential-backoff')
async function main() {
const retry = backoff.cached(/* options */)
const work = () => httpRequest('http://example.com')
for (let i = 0; i < 1000; i++) {
const response = await retry(work)
}
}
also cached iterator:
const backoff = require('@kessler/exponential-backoff')
async function main() {
const createIterator = backoff.cachedIterator(/* options */)
const work = () => httpRequest('http://example.com')
for (let i = 0; i < 1000; i++) {
const iterator = createIterator(work)
for await (const attempt of iterator) {}
}
}
Specifiying maxAttempts = Infinity can be handy in situations where you want to retry forever or control the maximum attempts dynamically.
This, however, will never through a operation failed, exceeded maximum attempts
error and implicitly ignores throwMaxAttemptsError
flag
const backoff = require('@kessler/exponential-backoff')
async function main() {
const work = attempt => {
if (attempt > computeMaxAttemptSomehow()) {
return
}
return httpRequest('http://example.com')
}
const response = await backoff(work, { maxAttempts: Infinity })
}
This module uses debug module. set DEBUG=@kessler/exponential-backoff
to show debug messages.
This module is based on the wikipedia article for exponential backoff
The use of the word attempt is used in the code instead of retry mostly because of the wikipedia article.
MIT © Yaniv Kessler