Retry with Exponential Backoff in Pure PHP
PHP Code Editor
Execution Result
Ready to execute
Click the "Run Script" button to see the output here
Description
Transient failures (network blips, lock contention, rate limits) are common in distributed systems. Retrying immediately after a failure often hits the same problem again. Exponential backoff solves this by increasing the wait time after each failure, giving the upstream service time to recover. Jitter (randomizing the delay within a range) prevents multiple clients from retrying in sync and creating a thundering herd.
The function wraps a callable in a loop that runs up to maxAttempts times. On each failure it computes the next delay as baseDelayMs * multiplier^attempt, caps it at maxDelayMs, then randomizes between 75% and 125% of that value using lcg_value(). The delay is applied with usleep() in microseconds. If all attempts are exhausted, the last caught exception is re-thrown. The attempt number is passed to the callable so callers can log retries or vary behavior per attempt.
<?php
/**
* Retry a callable with exponential backoff.
*
* @param callable $fn The operation to retry
* @param int $maxAttempts Maximum number of attempts
* @param int $baseDelayMs Initial delay in milliseconds
* @param float $multiplier Backoff multiplier (2.0 = double each time)
* @param int $maxDelayMs Cap on delay
* @throws Throwable Last exception if all attempts fail
*/
function retry(
callable $fn,
int $maxAttempts = 3,
int $baseDelayMs = 100,
float $multiplier = 2.0,
int $maxDelayMs = 5000,
): mixed {
$attempt = 0;
$delay = $baseDelayMs;
while (true) {
$attempt++;
try {
return $fn($attempt);
} catch (Throwable $e) {
if ($attempt >= $maxAttempts) {
throw $e;
}
// Add jitter: randomize between 75% and 125% of delay
$jittered = (int)($delay * (0.75 + lcg_value() * 0.5));
echo "Attempt $attempt failed: {$e->getMessage()}. Retrying in {$jittered}ms...\n";
usleep($jittered * 1000);
$delay = (int)min($maxDelayMs, $delay * $multiplier);
}
}
}
// Demo: simulate a flaky operation that fails the first two times
$callCount = 0;
try {
$result = retry(
fn(int $attempt) => (function () use (&$callCount): string {
$callCount++;
if ($callCount < 3) {
throw new RuntimeException("Connection timeout (call #$callCount)");
}
return "Success on call #$callCount";
})(),
maxAttempts: 5,
baseDelayMs: 50,
);
echo "Result: $result\n";
} catch (Throwable $e) {
echo "All attempts failed: " . $e->getMessage() . "\n";
}
// Demo: operation that always fails
echo "\n--- Always failing ---\n";
try {
retry(
fn() => throw new RuntimeException("Permanent error"),
maxAttempts: 3,
baseDelayMs: 10,
);
} catch (Throwable $e) {
echo "Gave up: " . $e->getMessage() . "\n";
}
Comments
No comments yet
Be the first to share your thoughts!