Skip to content
GreenSnippets

Retry a callable with exponential backoff in PHP

Re-run flaky operations with growing delays and jitter, only for chosen exception types.

PHP
<?php
/**
 * @template T
 * @param callable(): T $operation
 * @param array<class-string<Throwable>> $retryOn
 * @return T
 */
function retry(callable $operation, int $attempts = 3, int $baseDelayMs = 200, array $retryOn = [Exception::class], ?callable $sleep = null)
{
    $sleep ??= fn (int $ms) => usleep($ms * 1000);
    for ($attempt = 1; ; $attempt++) {
        try {
            return $operation();
        } catch (Throwable $e) {
            $retryable = array_filter($retryOn, fn ($class) => $e instanceof $class);
            if (!$retryable || $attempt >= $attempts) {
                throw $e;
            }
            $delay = $baseDelayMs * (2 ** ($attempt - 1));
            $sleep($delay + random_int(0, intdiv($delay, 2)));
        }
    }
}

Example / expected output

Output
$body = retry(fn () => fetchRemote($url), attempts: 4, retryOn: [RuntimeException::class]);

How it works

Use for idempotent operations such as HTTP GETs. Delays double each attempt, with random jitter so many workers do not retry in lockstep. Exceptions that are not in $retryOn propagate immediately, and the last exception is rethrown when attempts run out. The sleep function is injectable for testing.