PHPackages                             code-distortion/backoff - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. code-distortion/backoff

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

code-distortion/backoff
=======================

A PHP retry library implementing backoff strategies and jitter

0.1.2(5mo ago)0112—0%2MITPHPPHP 8.0.\* | 8.1.\* | 8.2.\* | 8.3.\* | 8.4.\* | 8.5.\*CI passing

Since Jan 7Pushed 5mo ago1 watchersCompare

[ Source](https://github.com/code-distortion/backoff)[ Packagist](https://packagist.org/packages/code-distortion/backoff)[ Docs](https://github.com/code-distortion/backoff)[ RSS](/packages/code-distortion-backoff/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (3)Dependencies (6)Versions (4)Used By (2)

Backoff
=======

[](#backoff)

[![Latest Version on Packagist](https://camo.githubusercontent.com/9b27d56b650f8bfca6f04965a2024965289741825cc7f7c99ceefe23a175117f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f636f64652d646973746f7274696f6e2f6261636b6f66662e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/code-distortion/backoff)[![PHP Version](https://camo.githubusercontent.com/0ab28fa42d406af6773a7087082e09d61424dd96d6598541a0a899640ea89b55/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e30253230746f253230382e352d626c75653f7374796c653d666c61742d737175617265)](https://camo.githubusercontent.com/0ab28fa42d406af6773a7087082e09d61424dd96d6598541a0a899640ea89b55/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e30253230746f253230382e352d626c75653f7374796c653d666c61742d737175617265)[![GitHub Workflow Status](https://camo.githubusercontent.com/0dc1678e05454d46c01e11492c4ad44db5e835cdede26240a496c0d5028d867e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f636f64652d646973746f7274696f6e2f6261636b6f66662f72756e2d74657374732e796d6c3f6272616e63683d6d61696e267374796c653d666c61742d737175617265)](https://github.com/code-distortion/backoff/actions)[![Buy The World a Tree](https://camo.githubusercontent.com/dc3f77a9b22c3bc83c7b7d863bf138a7ca3418f1826b0b16d073d0aa87c16bc4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f74726565776172652d2546302539462538432542332d6c69676874677265656e3f7374796c653d666c61742d737175617265)](https://plant.treeware.earth/code-distortion/backoff)[![Contributor Covenant](https://camo.githubusercontent.com/902d296a65b2997bada7e7717fd929d9177f3bd95414cbb5ea2ed843c680f314/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636f6e7472696275746f72253230636f76656e616e742d76322e3125323061646f707465642d6666363962342e7376673f7374796c653d666c61742d737175617265)](.github/CODE_OF_CONDUCT.md)

[![Sequence Backoff](docs/backoff-logo.png)](docs/backoff-logo.png)

***code-distortion/backoff*** is a PHP library that retries your actions when they fail. It implements various backoff strategies and jitter to avoid overwhelming the resource being accessed.

It's useful when you're working with services that might be temporarily unavailable, such as APIs.

```
// let Backoff manage the delays and trigger retries for you
$action = fn() => …; // do some work
$result = Backoff::exponential(2)->maxAttempts(10)->maxDelay(30)->attempt($action);
```

> See the [cheatsheet](#cheatsheet) for an overview of what's possible.

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [General Backoff Tips](#general-backoff-tips)
    - [Further Reading](#further-reading)
- [Cheatsheet](#cheatsheet)
- [Usage](#usage)
- [Backoff Algorithms](#backoff-algorithms)
    - [Fixed Backoff](#fixed-backoff)
    - [Linear Backoff](#linear-backoff)
    - [Exponential Backoff](#exponential-backoff)
    - [Polynomial Backoff](#polynomial-backoff)
    - [Fibonacci Backoff](#fibonacci-backoff)
    - [Decorrelated Backoff](#decorrelated-backoff)
    - [Random Backoff](#random-backoff)
    - [Sequence Backoff](#sequence-backoff)
    - [Callback Backoff](#callback-backoff)
    - [Custom Backoff Algorithm Class](#custom-backoff-algorithm-class)
    - [Noop Backoff](#noop-backoff)
    - [No Backoff](#no-backoff)
- [Configuration (Customise the Retry Logic)](#configuration-customise-the-retry-logic)
    - [Max Attempts](#max-attempts)
    - [Delay](#delay)
        - [Max-Delay](#max-delay)
        - [Immediate First Retry](#immediate-first-retry)
    - [Jitter](#jitter)
        - [Full Jitter](#full-jitter)
        - [Equal Jitter](#equal-jitter)
        - [Custom Jitter Range](#custom-jitter-range)
        - [Jitter Callback](#jitter-callback)
        - [Custom Jitter Class](#custom-jitter-class)
        - [No Jitter](#no-jitter)
- [Managing Exceptions](#managing-exceptions)
    - [Retry When Any Exception Occurs](#retry-when-any-exception-occurs)
    - [Retry When Particular Exceptions Occur](#retry-when-particular-exceptions-occur)
    - [Don't Retry When Exceptions Occur](#dont-retry-when-exceptions-occur)
- [Managing "Invalid" Return Values](#managing-invalid-return-values)
    - [Retry When…](#retry-when)
    - [Retry Until…](#retry-until)
- [Callbacks](#callbacks)
    - [Exception Callback](#exception-callback)
    - [Invalid Result Callback](#invalid-result-callback)
    - [Success Callback](#success-callback)
    - [Failure Callback](#failure-callback)
    - [Finally Callback](#finally-callback)
- [Logging](#logging)
    - [The AttemptLog Class](#the-attemptlog-class)
- [Working With Test Suites](#working-with-test-suites)
    - [Disabling Backoff Delays](#disabling-backoff-delays)
    - [Disabling Retries](#disabling-retries)
- [Managing the Retry Loop Yourself](#managing-the-retry-loop-yourself)
    - [The Basic Loop](#the-basic-loop)
    - [Catching Exceptions in Your Loop](#catching-exceptions-in-your-loop)
    - [Deconstructing the Backoff Logic](#deconstructing-the-backoff-logic)
    - [Working With Logs](#working-with-logs)
    - [Helpers When Managing The Loop Yourself](#helpers-when-managing-the-loop-yourself)
- [Modelling / Simulation](#modelling--simulation)

Installation
------------

[](#installation)

Install the package via composer:

```
composer require code-distortion/backoff
```

General Backoff Tips
--------------------

[](#general-backoff-tips)

- Backoff attempts are intended to be used when actions fail because of **transient** issues (such as temporary service outages). When **permanent** errors occur (such as a 404 HTTP response), [retrying should stop](#managing-exceptions) as it won't help.
- Be careful when nesting backoff attempts. This can unexpectedly increase the number of attempts and time taken.
- Actions taken during backoff attempts should be idempotent. Meaning, if the same action is performed multiple times, the outcome should be the same as if it were only performed once.

### Further Reading

[](#further-reading)

- The article [Timeouts, retries, and backoff with jitter](https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/) by Marc Brooker at AWS does a good job of explaining the concepts involved when using backoff strategies.
- The article [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) also by Marc Brooker is a good read if you're interested in the theory behind backoff algorithms and jitter. Marc [explains the same concepts in a 2019 talk](https://www.youtube.com/watch?v=sKRdemSirDM&t=1896s).

Cheatsheet
----------

[](#cheatsheet)

Quick examples…

```
// the usual case
$action = fn() => …; // do some work
$result = Backoff::exponential(2)->maxAttempts(10)->maxDelay(30)->attempt($action);

// selection of examples
$result = Backoff::exponential(1)->attempt($action, $default);
Backoff::polynomial(1)->attempt($action);
Backoff::sequence([1, 2, 3, 5, 10])->attempt($action);
Backoff::exponential(1)->equalJitter()->immediateFirstRetry()->attempt($action);
Backoff::exponential(1)->retryExceptions(MyException::class)->attempt($action);
Backoff::exponential(1)->retryWhen(false)->attempt($action);
Backoff::exponential(1)->retryUntil(true)->attempt($action);
Backoff::exponential(1)->failureCallback($failed)->attempt($action);
```

Start by picking an algorithm to use…

```
// backoff algorithms - in seconds
Backoff::fixed(2)                         // 2, 2, 2, 2, 2…
Backoff::linear(5)                        // 5, 10, 15, 20, 25…
Backoff::linear(5, 10)                    // 5, 15, 25, 35, 45…
Backoff::exponential(1)                   // 1, 2, 4, 8, 16…
Backoff::exponential(1, 1.5)              // 1, 1.5, 2.25, 3.375, 5.0625…
Backoff::polynomial(1)                    // 1, 4, 9, 16, 25…
Backoff::polynomial(1, 1.5)               // 1, 2.8284271247462, 5.1961524227066, 8, 11.180339887499…
Backoff::fibonacci(1)                     // 1, 1, 2, 3, 5…
Backoff::decorrelated(1)                  // 1.6147780669, 2.9651922732, 5.7128698436, 10.3225378844, 2.3890401166…
Backoff::random(2, 5)                     // 2.7361497528, 2.8163467878, 4.6468904857, 3.3016198676, 3.3810068137…
Backoff::sequence([1, 2, 3, 5, 10])       // 1, 2, 3, 5, 10
Backoff::sequence([1, 2, 3, 5, 10], true) // 1, 2, 3, 5, 10, 10, 10, 10, 10…
Backoff::callback($callback)              // $callback(1, $prev), $callback(2, $prev), $callback(3, $prev)…
Backoff::custom($backoffAlgorithm)        // delay managed by a custom backoff algorithm class

// backoff algorithms - in milliseconds
Backoff::fixedMs(2)                         // 2, 2, 2, 2, 2…
Backoff::linearMs(5)                        // 5, 10, 15, 20, 25…
Backoff::linearMs(5, 10)                    // 5, 15, 25, 35, 45…
Backoff::exponentialMs(1)                   // 1, 2, 4, 8, 16…
Backoff::exponentialMs(1, 1.5)              // 1, 1.5, 2.25, 3.375, 5.0625…
Backoff::polynomialMs(1)                    // 1, 4, 9, 16, 25…
Backoff::polynomialMs(1, 1.5)               // 1, 2.8284271247462, 5.1961524227066, 8, 11.180339887499…
Backoff::fibonacciMs(1)                     // 1, 1, 2, 3, 5…
Backoff::decorrelatedMs(1)                  // 1.6147780669, 2.9651922732, 5.7128698436, 10.3225378844, 2.3890401166…
Backoff::randomMs(2, 5)                     // 2.7361497528, 2.8163467878, 4.6468904857, 3.3016198676, 3.3810068137…
Backoff::sequenceMs([1, 2, 3, 5, 10])       // 1, 2, 3, 5, 10
Backoff::sequenceMs([1, 2, 3, 5, 10], true) // 1, 2, 3, 5, 10, 10, 10, 10, 10…
Backoff::callbackMs($callback)              // $callback(1, $prev), $callback(2, $prev), $callback(3, $prev)…
Backoff::customMs($backoffAlgorithm)        // delay managed by a custom backoff algorithm class

// backoff algorithms - in microseconds
Backoff::fixedUs(2)                         // 2, 2, 2, 2, 2…
Backoff::linearUs(5)                        // 5, 10, 15, 20, 25…
Backoff::linearUs(5, 10)                    // 5, 15, 25, 35, 45…
Backoff::exponentialUs(1)                   // 1, 2, 4, 8, 16…
Backoff::exponentialUs(1, 1.5)              // 1, 1.5, 2.25, 3.375, 5.0625…
Backoff::polynomialUs(1)                    // 1, 4, 9, 16, 25…
Backoff::polynomialUs(1, 1.5)               // 1, 2.8284271247462, 5.1961524227066, 8, 11.180339887499…
Backoff::fibonacciUs(1)                     // 1, 1, 2, 3, 5…
Backoff::decorrelatedUs(1)                  // 1.6147780669, 2.9651922732, 5.7128698436, 10.3225378844, 2.3890401166…
Backoff::randomUs(2, 5)                     // 2.7361497528, 2.8163467878, 4.6468904857, 3.3016198676, 3.3810068137…
Backoff::sequenceUs([1, 2, 3, 5, 10])       // 1, 2, 3, 5, 10
Backoff::sequenceUs([1, 2, 3, 5, 10], true) // 1, 2, 3, 5, 10, 10, 10, 10, 10…
Backoff::callbackUs($callback)              // $callback(1, $prev), $callback(2, $prev), $callback(3, $prev)…
Backoff::customUs($backoffAlgorithm)        // delay managed by a custom backoff algorithm class

// utility backoff algorithms
Backoff::noop() // 0, 0, 0, 0, 0…
Backoff::none() // (1 attempt, no retries)
```

Then customise the retry logic…

```
// max-attempts (default = no limit)
->maxAttempts(10)   // the maximum number of attempts allowed
->maxAttempts(null) // remove the limit, or
->noMaxAttempts()   // remove the limit, or
->noAttemptLimit()  // alias for noMaxAttempts()

// max-delay - the maximum delay to wait between each attempt (default = no limit)
->maxDelay(30)   // set the max-delay, in the current unit-of-measure
->maxDelay(null) // remove the limit, or
->noMaxDelay()   // remove the limit, or
->noDelayLimit() // alias for noMaxDelay()

// choose the type of jitter to apply to the delay (default = full jitter)
->fullJitter()              // apply full jitter, between 0% and 100% of the base-delay (applied by default)
->equalJitter()             // apply equal jitter, between 50% and 100% of the base-delay
->jitterRange(0.75, 1.25)   // apply jitter between $min and $max (e.g. 0.75 = 75%, 1.25 = 125%) of the base-delay
->jitterCallback($callback) // specify a callback that applies the jitter
->customJitter($jitter)     // jitter managed by a custom jitter class
->noJitter()                // disable jitter - the base-delay will be used as-is

// insert an initial retry that happens straight away
// before the backoff algorithm starts generating delays (default = off)
->immediateFirstRetry()      // insert an immediate retry
->immediateFirstRetry(false) // don't insert an immediate retry, or
->noImmediateFirstRetry()    // don't insert an immediate retry

// turn off delays or retries altogether - may be useful when running tests (default = enabled)
->onlyDelayWhen(!$runningTests) // enable or disable delays (disabled means delays are 0)
->onlyRetryWhen(!$runningTests) // enable or disable retries (disabled means only 1 attempt will be made)
```

Retry only in certain situations if you'd like…

```
// retry based on EXCEPTIONS…

// retry when any exception occurs (this is the default setting)
// along with $default which is returned if all attempts fail
// $default may be a callable that returns the default value
// if $default is omitted, the final exception will be rethrown
->retryAllExceptions()
->retryAllExceptions($default)

// retry when these particular exceptions occur
// (you can specify multiple types of exceptions by passing
// them as an array, or by calling this multiple times)
->retryExceptions(MyException::class)
->retryExceptions(MyException::class, $default)

// you can also specify a callback that chooses whether to retry or not
// (return true to retry, false to end)
// $callback(Throwable $e, AttemptLog $log): bool
->retryExceptions($callback);
->retryExceptions($callback, $default);

// or choose to NOT retry when exceptions occur
// if $default is omitted, any exceptions will be rethrown
->retryExceptions(false) // or
->dontRetryExceptions()
->retryExceptions(false, $default) // or
->dontRetryExceptions($default)
```

```
// retry based on the return VALUE…
// (by default, retries won't happen based on the return value)

// retry WHEN a particular value is returned,
// along with $default which is returned if all attempts fail
// $default may be a callable that returns the default value
// if $default is omitted, the final value returned by $action is returned
// (you can check for different values by calling this multiple times)
->retryWhen($match, $strict = false)
->retryWhen($match, $strict, $default)

// you can also specify a callback that chooses whether to retry or not
// (return true to retry, false to end)
// $callback(mixed $result, AttemptLog $log): bool
->retryWhen($callback)
->retryWhen($callback, false, $default) // strict doesn't matter when using a callback

// retry UNTIL this value is returned
// (you can check for different values by calling this multiple times)
->retryUntil($match, $strict = false)

// you can also pass a callback that chooses whether to retry or not
// (unlike ->retryWhen(…), here you return false to retry, true to end)
// $callback(mixed $result, AttemptLog $log): bool
->retryUntil($callback)
```

Add callbacks if desired…

```
// (you can specify multiple callbacks at a time by passing
// them as an array, or by calling these methods multiple times)

// called when any exception occurs
// $callback(Throwable $e, AttemptLog $log, bool $willRetry): void
->exceptionCallback($callback)

// called when an "invalid" value is returned
// $callback(mixed $result, AttemptLog $log, bool $willRetry): void
->invalidResultCallback($callback)

// called after an attempt succeeds
// $callback(AttemptLog[] $logs): void
->successCallback($callback)

// called after all attempts fail, including when no
// attempts occur, and when an exception is thrown
// $callback(AttemptLog[] $logs): void
->failureCallback($callback)

// called afterwards regardless of the outcome, including
// when no attempts occur, and when an exception is thrown
// $callback(AttemptLog[] $logs): void
->finallyCallback($callback)
```

And finally, run your work…

```
->attempt($action);           // run your callback and retry it when needed
->attempt($action, $default); // run your callback, retry it when needed, and return $default if all attempts fail
                              // $default may be a callable that returns the default value
```

Usage
-----

[](#usage)

Start by:

- picking an [algorithm](#backoff-algorithms) to use (which calculates the length of each delay),
- [customise the retry logic](#configuration-customise-the-retry-logic) as needed,
- and then use it to run your work by passing closure `$action` to `->attempt($action)`.

By default, your closure will be retried [when *exceptions* occur](#managing-exceptions). The value returned by your closure will be returned when it succeeds.

```
use CodeDistortion\Backoff\Backoff;

$action = fn() => …; // do some work
$result = Backoff::exponential(1)->maxDelay(30)->maxAttempts(10)->attempt($action);
```

When exceptions occur, the final exception is rethrown.

However, you can pass a default value to return instead.

```
$result = Backoff::exponential(1)->maxDelay(30)->maxAttempts(10)->attempt($action, $default);
```

> ***Note:*** `$default` may be a *callable* that returns the default value. It will only be called when the default value is needed.

Backoff Algorithms
------------------

[](#backoff-algorithms)

Backoff algorithms are used to calculate how long to wait between attempts. They usually increase the delay between attempts in some way.

> ***Note:*** The actual delays will vary because [Jitter](#jitter) is applied to the delays the algorithms generate. This is designed to avoid the [*Thundering Herd* problem](https://nick.groenen.me/notes/thundering-herd/) by making retries less predictable.

By default, delays are in seconds. However, each algorithm has millisecond and microsecond variations.

> ***Note:*** Delays in any unit-of-measure can have decimal places, including seconds.

> ***Note:*** Microseconds are probably small enough that the numbers start to become inaccurate because of PHP overheads when sleeping. For example, on my computer, while code can run quicker than a microsecond, running usleep(1) to sleep for 1 microsecond actually takes about 55 microseconds.

A number of backoff algorithms have been included to choose from, and you can also [create your own](#custom-backoff-algorithm-class)…

### Fixed Backoff

[](#fixed-backoff)

[![Fixed Backoff](docs/algorithms/fixed-backoff.png)](docs/algorithms/fixed-backoff.png)

The fixed backoff algorithm waits the *same* amount of time between each attempt.

```
// Backoff::fixed($delay)

Backoff::fixed(2)->attempt($action); // 2, 2, 2, 2, 2…

Backoff::fixedMs(2)->attempt($action); // in milliseconds
Backoff::fixedUs(2)->attempt($action); // in microseconds
```

### Linear Backoff

[](#linear-backoff)

[![Linear Backoff](docs/algorithms/linear-backoff.png)](docs/algorithms/linear-backoff.png)

The linear backoff algorithm increases the waiting period by a specific amount each time.

If `$delayIncrease` is not passed, it will increase by `$initalDelay` each time.

`Logic: $delay = $initialDelay + (($retryNumber - 1) * $delayIncrease)`

```
// Backoff::linear($initialDelay, $delayIncrease = null)

Backoff::linear(5)->attempt($action);     // 5, 10, 15, 20, 25…
Backoff::linear(5, 10)->attempt($action); // 5, 15, 25, 35, 45…

Backoff::linearMs(5)->attempt($action); // in milliseconds
Backoff::linearUs(5)->attempt($action); // in microseconds
```

### Exponential Backoff

[](#exponential-backoff)

[![Exponential Backoff](docs/algorithms/exponential-backoff.png)](docs/algorithms/exponential-backoff.png)

The exponential backoff algorithm increases the waiting period exponentially.

By default, the delay is doubled each time, but you can change the factor it multiplies by.

`Logic: $delay = $initialDelay * pow($factor, $retryNumber - 1)`

```
// Backoff::exponential($initialDelay, $factor = 2)

Backoff::exponential(1)->attempt($action);      // 1, 2, 4, 8, 16…
Backoff::exponential(1, 1.5)->attempt($action); // 1, 1.5, 2.25, 3.375, 5.0625…

Backoff::exponentialMs(1)->attempt($action); // in milliseconds
Backoff::exponentialUs(1)->attempt($action); // in microseconds
```

### Polynomial Backoff

[](#polynomial-backoff)

[![Polynomial Backoff](docs/algorithms/polynomial-backoff.png)](docs/algorithms/polynomial-backoff.png)

The polynomial backoff algorithm increases the waiting period in a polynomial manner.

By default, the retry number is raised to the power of 2, but you can change this.

`Logic: $delay = $initialDelay * pow($retryNumber, $power)`

```
// Backoff::polynomial($initialDelay, $power = 2)

Backoff::polynomial(1)->attempt($action);      // 1, 4, 9, 16, 25…
Backoff::polynomial(1, 1.5)->attempt($action); // 1, 2.8284271247462, 5.1961524227066, 8, 11.180339887499…

Backoff::polynomialMs(1)->attempt($action); // in milliseconds
Backoff::polynomialUs(1)->attempt($action); // in microseconds
```

### Fibonacci Backoff

[](#fibonacci-backoff)

[![Fibonacci Backoff](docs/algorithms/fibonacci-backoff.png)](docs/algorithms/fibonacci-backoff.png)

The Fibonacci backoff algorithm increases waiting period by following a Fibonacci sequence. This is where each delay is the sum of the previous two delays.

`Logic: $delay = $previousDelay1 + $previousDelay2`

```
// Backoff::fibonacci($initialDelay, $includeFirst = false)

Backoff::fibonacci(1)->attempt($action); // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
Backoff::fibonacci(5)->attempt($action); // 5, 5, 10, 15, 25, 40, 65, 105, 170, 275…

Backoff::fibonacciMs(1)->attempt($action); // in milliseconds
Backoff::fibonacciUs(1)->attempt($action); // in microseconds
```

Seeing as the first and second delays in a Fibonacci sequence are the same, you can choose to skip the first delay if you like.

```
Backoff::fibonacci(1, false)->attempt($action); // 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
Backoff::fibonacci(5, false)->attempt($action); // 5, 10, 15, 25, 40, 65, 105, 170, 275, 445…
```

### Decorrelated Backoff

[](#decorrelated-backoff)

[![Decorrelated Backoff](docs/algorithms/decorrelated-backoff.png)](docs/algorithms/decorrelated-backoff.png)

The decorrelated backoff algorithm is a feedback loop where the previous delay is used as input to help to determine the next delay.

A random delay between the `$baseDelay` and the `previous-delay * 3` is picked.

Jitter is not applied to this algorithm.

`Logic: $delay = rand($baseDelay, $prevDelay * $multiplier)`

```
// Backoff::random($baseDelay, $multiplier = 3)

Backoff::decorrelated(1)->attempt($action); // 2.6501523185, 7.4707976956, 12.3241439061, 25.1076970005, 46.598982162…
Backoff::decorrelated(1, 2)->attempt($action); // 1.6147780669, 2.9651922732, 5.7128698436, 10.3225378844, 2.3890401166…

Backoff::decorrelatedMs(1)->attempt($action); // in milliseconds
Backoff::decorrelatedUs(1)->attempt($action); // in microseconds
```

> ***Info:*** The article [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) by Marc Brooker at AWS explains Decorrelated Backoff in more detail.

### Random Backoff

[](#random-backoff)

[![Random Backoff](docs/algorithms/random-backoff.png)](docs/algorithms/random-backoff.png)

The random backoff algorithm waits for a random period of time within the range you specify.

Jitter is not applied to this algorithm.

`Logic: $delay = rand($min, $max)`

```
// Backoff::random($min, $max)

Backoff::random(2, 5)->attempt($action); // 2.7361497528, 2.8163467878, 4.6468904857, 3.3016198676, 3.3810068137…

Backoff::randomMs(2, 5)->attempt($action); // in milliseconds
Backoff::randomUs(2, 5)->attempt($action); // in microseconds
```

### Sequence Backoff

[](#sequence-backoff)

[![Sequence Backoff](docs/algorithms/sequence-backoff.png)](docs/algorithms/sequence-backoff.png)

The sequence backoff algorithm lets you specify the particular delays to use.

An optional fixed delay can be used to continue with, after the sequence finishes. Otherwise, the attempts will stop when the sequence has been exhausted.

> ***Note:*** You'll need to make sure the delay values you specify match the unit-of-measure being used.

`Logic: $delay = $delays[$retryNumber - 1]`

```
// Backoff::sequence($delays, $continuation = null)

Backoff::sequence([1, 1.25, 1.5, 2, 3])->attempt($action);       // 1, 1.25, 1.5, 2, 3
Backoff::sequence([1, 1.25, 1.5, 2, 3], true)->attempt($action); // 1, 1.25, 1.5, 2, 3, 3, 3, 3, 3…

Backoff::sequenceMs([1, 1.25, 1.5, 2, 3])->attempt($action); // in milliseconds
Backoff::sequenceUs([1, 1.25, 1.5, 2, 3])->attempt($action); // in microseconds
```

> ***Note:*** If you [use `->immediateFirstRetry()`](#immediate-first-retry), one extra retry will be made before your sequence starts.

### Callback Backoff

[](#callback-backoff)

[![Sequence Backoff](docs/algorithms/callback-backoff.png)](docs/algorithms/callback-backoff.png)

The callback backoff algorithm lets you specify a callback that chooses the period to wait.

Your callback is expected to return an `int` or `float` representing the delay, or `null` to indicate that the attempts should stop.

`Logic: $delay = $callback($retryNumber, $prevBaseDelay)`

```
// $callback = function (int $retryNumber, int|float|null $prevBaseDelay): int|float|null …

Backoff::callback($callback)->attempt($action); // $callback(1, $prev), $callback(2, $prev), $callback(3, $prev)…

Backoff::callbackMs($callback)->attempt($action); // in milliseconds
Backoff::callbackUs($callback)->attempt($action); // in microseconds
```

> ***Note:*** You'll need to make sure the delay values you return match the unit-of-measure being used.

> ***Note:*** If you [use `->immediateFirstRetry()`](#immediate-first-retry), one extra retry will be made before your callback is used.
>
> In this case, `$retryNumber` will start with 1, but it will really be for the second attempt onwards.

### Custom Backoff Algorithm Class

[](#custom-backoff-algorithm-class)

[![Sequence Backoff](docs/algorithms/custom-backoff.png)](docs/algorithms/custom-backoff.png)

As well as the [callback option above](#callback-backoff), you have the ability to create your own backoff algorithm *class* by extending `BaseBackoffAlgorithm` and implementing the `BackoffAlgorithmInterface`.

```
// MyBackoffAlgorithm.php

use CodeDistortion\Backoff\Interfaces\BackoffAlgorithmInterface;
use CodeDistortion\Backoff\Support\BaseBackoffAlgorithm;

class MyBackoffAlgorithm extends BaseBackoffAlgorithm implements BackoffAlgorithmInterface
{
    /** @var boolean Whether jitter may be applied to the delays calculated by this algorithm. */
    protected bool $jitterMayBeApplied = true;

    public function __construct(
        // e.g. private int|float $initialDelay,
        // … and any other parameters you need
    ) {
    }

    public function calculateBaseDelay(int $retryNumber, int|float|null $prevBaseDelay): int|float|null
    {
        return …; // your logic here
    }
}
```

Then use your custom backoff algorithm like this:

```
$algorithm = new MyBackoffAlgorithm(…);

Backoff::custom($algorithm)->attempt($action);

Backoff::customMs($algorithm)->attempt($action); // in milliseconds
Backoff::customUs($algorithm)->attempt($action); // in microseconds
```

> ***Note:*** You'll need to make sure the delay values you return match the unit-of-measure being used.

> ***Note:*** If you [use `->immediateFirstRetry()`](#immediate-first-retry), an extra retry will be made before your algorithm is used.
>
> In this case, `$retryNumber` will start with 1, but it will really be for the second attempt onwards.

### Noop Backoff

[](#noop-backoff)

[![Noop Backoff](docs/algorithms/noop-backoff.png)](docs/algorithms/noop-backoff.png)

The "no-op" backoff algorithm is a utility algorithm that doesn't wait at all, retries are attempted straight away.

This might be useful for testing purposes. See [Working With Test Suites](#working-with-test-suites) for more options when running tests.

```
Backoff::noop()->attempt($action); // 0, 0, 0, 0, 0…
```

### No Backoff

[](#no-backoff)

The "no backoff" algorithm is a utility algorithm that doesn't allow retries at all. Only the first attempt will be made.

This might be useful for testing purposes. See [Working With Test Suites](#working-with-test-suites) for more options when running tests.

```
Backoff::none()->attempt($action); // (no retries)
```

Configuration (Customise the Retry Logic)
-----------------------------------------

[](#configuration-customise-the-retry-logic)

### Max Attempts

[](#max-attempts)

By default, Backoff will retry forever. To stop this from happening, you can specify the maximum number of attempts allowed.

```
Backoff::exponential(1)
    ->maxAttempts(5) // immediateFirstRetry()`. This will be inserted before the normal backoff delays start.

```
Backoff::exponential(10)
    ->maxAttempts(5)
    ->immediateFirstRetry() // jitterRange(0.75, 1.25) //  …; // your logic here

Backoff::exponential(1)
    ->jitterCallback($callback) // retryExceptions(false) // retryUntil($match, $strict = false) //  ***Note:*** `$strict` has no effect when using a callback.

```
$callback = fn(mixed $result, AttemptLog $log): bool => …; // your logic here

Backoff::exponential(1)
    ->retryUntil($callback) // exceptionCallback(…)`.

It doesn't matter if the exception is caught using [-&gt;retryExceptions(…)](#retry-when-particular-exceptions-occur) or not. These callbacks will be called regardless of a retry being made afterwards.

```
$callback = fn(…) => …; // do something here

// the callback can accept these parameters:
//   $e              - called '$e' - the exception that was thrown
//   $exception      - called '$exception' - the exception that was thrown
//   Throwable $e    - of type 'Throwable', or any exception type you'd like to catch in particular
//   $willRetry      - called '$willRetry' - true if a retry will be made, false if not
//   AttemptLog $log - of type 'AttemptLog' - the current AttemptLog object
//   $log            - called '$log' - the current AttemptLog object
//   $logs           - called '$logs' - an array of AttemptLog objects

Backoff::exponential(1)
    ->exceptionCallback($callback) // exceptionCallback(…)` multiple times. Type-hint the `$exception` parameter differently each time. e.g.
>
> ```
> $callback1 = fn(MyException1 $e) => …; // will be called when MyException1 is thrown
> $callback2 = fn(MyException2 $e) => …; // will be called when MyException2 is thrown
>
> Backoff::exponential(1)
>     ->exceptionCallback($callback1, $callback2) //  ```
>
>
>
> Callbacks that match the exception type will be called.

### Invalid Result Callback

[](#invalid-result-callback)

If you'd like to run some code *each time* an invalid *result* is returned, you can pass a callback to `->invalidResultCallback(…)`.

```
$callback = fn(…) => …; // do something here

// the callback can accept these parameters:
//   $result         - called '$result' - the result that was returned
//   $willRetry      - called '$willRetry' - true if a retry will be made, false if not
//   AttemptLog $log - of type 'AttemptLog' - the current AttemptLog object
//   $log            - called '$log' - the current AttemptLog object
//   $logs           - called '$logs' - an array of AttemptLog objects

Backoff::exponential(1)
    ->invalidResultCallback($callback) //  …; // do something here

// the callback can accept these parameters:
//   $result         - called '$result' - the result that was returned
//   AttemptLog $log - of type 'AttemptLog' - the current AttemptLog object
//   $log            - called '$log' - the current AttemptLog object
//   $logs           - called '$logs' - an array of AttemptLog objects

Backoff::exponential(1)
    ->successCallback($callback) //  …; // do something here

// the callback can accept these parameters:
//   AttemptLog $log - of type 'AttemptLog' - the current AttemptLog object
//   $log            - called '$log' - the current AttemptLog object
//   $logs           - called '$logs' - an array of AttemptLog objects

Backoff::exponential(1)
    ->failureCallback($callback) //  …; // do something here

// the callback can accept these parameters:
//   AttemptLog $log - of type 'AttemptLog' - the current AttemptLog object
//   $log            - called '$log' - the current AttemptLog object
//   $logs           - called '$logs' - an array of AttemptLog objects

Backoff::exponential(1)
    ->finallyCallback($callback) // attemptNumber(); // the attempt being made (1, 2, 3…)
$log->retryNumber();   // the retry being made (0, 1, 2…)

// the maximum possible attempts
// (returns null for unlimited attempts)
// note: it's possible for a backoff algorithm to return null
// so the attempts finish early. This won't be reflected here
$log->maxAttempts();

$log->firstAttemptOccurredAt(); // when the first attempt started
$log->thisAttemptOccurredAt();  // when the current attempt started

// the time spent on this attempt
// (will be null until known)
$log->workingTime();          // in the current unit-of-measure
$log->workingTimeInSeconds(); // in seconds
$log->workingTimeInMs();      // in milliseconds
$log->workingTimeInUs();      // in microseconds

// the overall time spent attempting the action (so far)
// (sum of all working time since the first attempt)
// (will be null until known)
$log->overallWorkingTime();          // in the current unit-of-measure
$log->overallWorkingTimeInSeconds(); // in seconds
$log->overallWorkingTimeInMs();      // in milliseconds
$log->overallWorkingTimeInUs();      // in microseconds

// the delay that was applied before this attempt
// (will be null for the first attempt)
$log->prevDelay();          // in the current unit-of-measure
$log->prevDelayInSeconds(); // in seconds
$log->prevDelayInMs();      // in milliseconds
$log->prevDelayInUs();      // in microseconds

// the delay that will be used before the next attempt
// (will be null if there are no more attempts left)
$log->nextDelay();          // in the current unit-of-measure
$log->nextDelayInSeconds(); // in seconds
$log->nextDelayInMs();      // in milliseconds
$log->nextDelayInUs();      // in microseconds

// the overall delay so far (sum of all delays since the first attempt)
$log->overallDelay();          // in the current unit-of-measure
$log->overallDelayInSeconds(); // in seconds
$log->overallDelayInMs();      // in milliseconds
$log->overallDelayInUs();      // in microseconds

// the unit-of-measure used
// these are values from CodeDistortion\Backoff\Settings::UNIT_XXX
$log->unitType();
```

Working With Test Suites
------------------------

[](#working-with-test-suites)

When running your test-suite, you might want to disable the backoff delays, or stop retries altogether.

### Disabling Backoff Delays

[](#disabling-backoff-delays)

You can remove the delay between attempts using `->onlyDelayWhen(false)`.

The action may still be retried, but there won't be any delays between attempts.

```
$runningTests = …;

Backoff::exponential(1)
    ->maxAttempts(10)
    // 0, 0, 0, 0, 0… delays when running tests
    ->onlyDelayWhen(!$runningTests) //
> - equivalent to setting `->maxDelay(0)`, and
> - is largely equivalent to using the `Backoff::noop()` backoff.

### Disabling Retries

[](#disabling-retries)

Alternatively, you can disable retries altogether using `->onlyRetryWhen(false)`.

```
$runningTests = …;

$backoff = Backoff::exponential(1)
    ->maxAttempts(10)
    // no reties when running tests
    ->onlyRetryWhen(!$runningTests) //
> - setting `->maxAttempts(1)`, or
> - using the `Backoff::none()` backoff algorithm.

Managing the Retry Loop Yourself
--------------------------------

[](#managing-the-retry-loop-yourself)

If you'd like more control over the process, you can manage the retry loop yourself. This involves setting up a loop, and using Backoff to handle the delays each iteration.

Please note that by doing this, you're skipping the part of Backoff that manages the loop and retry process. You're essentially handling them yourself.

This means that you won't be able to use Backoff's functionality to:

- catch and retry because of [exceptions](#managing-exceptions) or [certain values being returned](#managing-invalid-return-values),
- or trigger [callbacks](#callbacks).

If your aim is to do one of the following, you could use one of the already available options:

> - [Catch and retry exceptions](#managing-exceptions) - If you'd like Backoff to catch *particular* exceptions, you can use [-&gt;retryExceptions(…)](#retry-when-particular-exceptions-occur). This lets you specify which exceptions to retry, or specify a callback to make the decision.
> - [Retry based on return values](#managing-invalid-return-values) - If you'd like to selectively retry based on particular *return values*, you can use [-&gt;retryWhen(…)](#retry-when) or [-&gt;retryUntil(…)](#retry-until). These let you specify values to check for, or specify a callback to make the decision.
> - [Trigger callbacks](#callbacks) - If you'd like to perform tasks in certain situations (like logging before each retry delay), you could consider using the callback options such as [-&gt;exceptionCallback(…)](#exception-callback) or [-&gt;invalidResultCallback(…)](#invalid-result-callback).

### The Basic Loop

[](#the-basic-loop)

Start by:

- picking a [backoff algorithm](#backoff-algorithms) and [configure it](#configuration-customise-the-retry-logic) as you normally would,
- incorporate it into your loop,
- call `->step()` to proceed to the next attempt. This sleeps for the appropriate amount of time, and returns `false` when the attempts have been exhausted.

```
use CodeDistortion\Backoff\Backoff;

// choose a backoff algorithm and configure it as needed
$backoff = Backoff::exponential(1)->maxDelay(30)->maxAttempts(10);

// then use it in your loop
do {
    $success = …; // do some work
} while ((!$success) && ($backoff->step())); // step() will be called at the entrance to your loop
$backoff = Backoff::exponential(1)->maxDelay(30)->maxAttempts($maxAttempts)->runsAtStartOfLoop(); // runsAtStartOfLoop()
    $backoff->sleep(); // getDelayInMs()`, or `->getDelayInUs()` to retrieve the delay in the unit-of-measure you need.

```
$backoff = Backoff::exponential(1)->maxDelay(30)->maxAttempts(10)->runsAtStartOfLoop();

$success = false;
while ((!$success) && ($backoff->step(false))) { // step() (default = at the end of the loop)
->runsAtStartOfLoop()      // specify that $backoff->step() will be called at the entrance to your loop
->runsAtStartOfLoop(false) // specify that $backoff->step() will be called at the end of your loop (default), or
->runsAtEndOfLoop()        // specify that $backoff->step() will be called at the end of your loop (default)

// trigger the backoff logic - placed in the structure of your loop
->step(); // calculate the delay and perform the sleep, returns false when the attempts are exhausted

// if you'd like to separate the sleep from ->step()
->step(false); // calculate delay without sleeping, returns false when the attempts are exhausted
->sleep();     // sleep for the delay calculated by ->step(false)

// if you'd like to perform the sleep yourself, call ->step(false) and then retrieve the delay
->getDelay();          // get the delay in the current unit-of-measure (note: may contain decimals)
->getDelayInSeconds(); // get the delay in seconds (note: may contain decimals)
->getDelayInMs();      // get the delay in milliseconds (note: may contain decimals)
->getDelayInUs();      // get the delay in microseconds (note: may contain decimals)
->getUnitType();       // get the unit-of-measure being used (from CodeDistortion\Backoff\Settings::UNIT_XXX)

// querying the state of the backoff
->currentAttemptNumber(); // get the current attempt number
->isFirstAttempt();       // check if the first attempt is currently being made
->isLastAttempt();        // check if the last attempt is currently being made (however it may run indefinitely)
->canContinue();          // check if the more attempts can be made - this is the same as what ->step() returns
->hasStopped();           // check if the attempts have been exhausted - this is the opposite to ->canContinue()

// working with logs
->startOfAttempt(); // start the attempt, so the log is built
->endOfAttempt();   // end the attempt, so the log is built
->currentLog();     // get the AttemptLog for the current attempt
->logs();           // get all the AttemptLogs (so far)

// and finally
->reset(); // reset the backoff to its initial state, ready to be re-used
```

Modelling / Simulation
----------------------

[](#modelling--simulation)

If you would like to run modelling on the backoff process, you can use a `Backoff` instance to generate sets of delays without actually sleeping.

```
// generate delays in the current unit-of-measure
$backoff->simulate(1);      // generate a single delay (e.g. for retry 1)
$backoff->simulate(10, 20); // generate a sequence of delays, returned as an array (e.g. for retries 10 - 20)
```

Equivalent methods exist to retrieve the delays in seconds, milliseconds and microseconds.

```
// generate delays in seconds (note: may contain decimals)
$backoff->simulateInSeconds(1);
$backoff->simulateInSeconds(1, 20);

// generate delays in milliseconds (note: may contain decimals)
$backoff->simulateInMs(1);
$backoff->simulateInMs(1, 20);

// generate delays in microseconds (note: may contain decimals)
$backoff->simulateInUs(1);
$backoff->simulateInUs(1, 20);
```

And just in case you need to check, you can retrieve the unit-of-measure being used.

```
// these are values from CodeDistortion\Backoff\Settings::UNIT_XXX
$backoff->getUnitType();
```

A `null` value in the results indicates that the attempts have been exhausted.

> ***Note:*** These methods will generate the same values when you call them again. Backoff maintains this state because some [backoff algorithms](#backoff-algorithms) base their delays on previously generated delays (e.g. the [decorrelated backoff algorithm](#decorrelated-backoff) does this), so their values are important.
>
> That is to say, when generating `$backoff->simulate(1, 20);` and then `$backoff->simulate(21, 40);`, the second set may be based on the first set.
>
> To generate a *new* set of delays, call `$backoff->reset()` first.
>
> ```
> $first = $backoff->simulate(1, 20);
> $second = $backoff->simulate(1, 20);
> // $second will be the same as $first
> $third = $backoff->reset()->simulate(1, 20);
> // however $third will be different
> ```

> ***Info:*** If these methods don't work fast enough for you, you could look into the `DelayCalculator` class, which `Backoff` uses behind the scenes to calculate the delays.
>
> Generate delays with it, and then call `$delayCalculator->reset()` before generating a new set.

Testing This Package
--------------------

[](#testing-this-package)

- Clone this package: `git clone https://github.com/code-distortion/backoff.git .`
- Run `composer install` to install dependencies
- Run the tests: `composer test`

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

### SemVer

[](#semver)

This library uses [SemVer 2.0.0](https://semver.org/) versioning. This means that changes to `X` indicate a breaking change: `0.0.X`, `0.X.y`, `X.y.z`. When this library changes to version 1.0.0, 2.0.0 and so forth, it doesn't indicate that it's necessarily a notable release, it simply indicates that the changes were breaking.

Treeware
--------

[](#treeware)

This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/code-distortion/backoff) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.

### Code of Conduct

[](#code-of-conduct)

Please see [CODE\_OF\_CONDUCT](.github/CODE_OF_CONDUCT.md) for details.

### Security

[](#security)

If you discover any security related issues, please email  instead of using the issue tracker.

Credits
-------

[](#credits)

- [Tim Chandler](https://github.com/code-distortion)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

37

—

LowBetter than 83% of packages

Maintenance70

Regular maintenance activity

Popularity12

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~160 days

Total

3

Last Release

176d ago

PHP version history (2 changes)0.1.0PHP 8.0.\* | 8.1.\* | 8.2.\* | 8.3.\* | 8.4.\*

0.1.2PHP 8.0.\* | 8.1.\* | 8.2.\* | 8.3.\* | 8.4.\* | 8.5.\*

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/56794290?v=4)[Tim](/maintainers/code-distortion)[@code-distortion](https://github.com/code-distortion)

---

Top Contributors

[![code-distortion](https://avatars.githubusercontent.com/u/56794290?v=4)](https://github.com/code-distortion "code-distortion (5 commits)")

---

Tags

retryrate limitingJitterstampedebackoffexponentialfibonaccipolynomialthundering herd

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/code-distortion-backoff/health.svg)

```
[![Health](https://phpackages.com/badges/code-distortion-backoff/health.svg)](https://phpackages.com/packages/code-distortion-backoff)
```

###  Alternatives

[eventsauce/backoff

Back-off strategy interface

70811.9k6](/packages/eventsauce-backoff)[davedevelopment/stiphle

Simple rate limiting/throttling for php

2567.7M9](/packages/davedevelopment-stiphle)[bandwidth-throttle/token-bucket

Implementation of the Token Bucket algorithm.

5121.9M10](/packages/bandwidth-throttle-token-bucket)[yriveiro/php-backoff

Simple backoff / retry functionality

2675.1k1](/packages/yriveiro-php-backoff)[vkartaviy/retry

The library for repeatable and retryable operations

29227.2k2](/packages/vkartaviy-retry)[tobion/retry

A generic library to retry an operation in case of an error. You can configure the behavior like the exceptions to retry on.

16396.8k](/packages/tobion-retry)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
