Installation
composer require yriveiro/php-backoff
Add to composer.json if not auto-loaded:
"autoload": {
"psr-4": {
"App\\": "app/",
"Yriveiro\\Backoff\\": "vendor/yriveiro/php-backoff/src/"
}
}
Run composer dump-autoload.
First Use Case Retry a failing HTTP request with exponential backoff:
use Yriveiro\Backoff\Backoff;
use Yriveiro\Backoff\ExponentialBackoff;
$backoff = new ExponentialBackoff(100, 3); // 100ms base delay, 3 retries
$attempt = 0;
while ($attempt <= 3) {
try {
$response = Http::get('https://api.example.com/data');
if ($response->successful()) {
return $response->json();
}
throw new \RuntimeException('Request failed');
} catch (\Exception $e) {
if ($attempt < 3) {
$backoff->sleep();
$attempt++;
} else {
throw $e;
}
}
}
Where to Look First
Retryable HTTP Requests Wrap Laravel HTTP client calls in a retry loop:
public function fetchWithRetry(string $url, int $maxRetries = 3)
{
$backoff = new ExponentialBackoff(200, $maxRetries);
$attempt = 0;
while ($attempt <= $maxRetries) {
try {
return Http::retry($maxRetries, $backoff->getDelay($attempt))
->get($url)
->throw();
} catch (\Exception $e) {
$attempt++;
if ($attempt <= $maxRetries) {
$backoff->sleep();
}
}
}
throw new \RuntimeException('Max retries exceeded');
}
Database Operations Retry on deadlocks or transient failures:
public function retryableTransaction(Closure $callback)
{
$backoff = new ExponentialBackoff(500, 3);
$attempt = 0;
while ($attempt <= 3) {
try {
return DB::transaction($callback);
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'deadlock')) {
$attempt++;
$backoff->sleep();
} else {
throw $e;
}
}
}
throw new \RuntimeException('Transaction failed after retries');
}
Custom Backoff Strategies
Implement BackoffInterface for non-exponential delays:
class FixedBackoff implements BackoffInterface
{
private $delay;
public function __construct(int $delayMs)
{
$this->delay = $delayMs;
}
public function sleep(): void
{
usleep($this->delay * 1000);
}
public function getDelay(int $attempt): int
{
return $this->delay;
}
}
$this->app->singleton(BackoffInterface::class, function ($app) {
return new ExponentialBackoff(100, 3);
});
public function handle($request, Closure $next)
{
$backoff = app(BackoffInterface::class);
$attempt = 0;
while ($attempt <= 3) {
try {
return $next($request);
} catch (\Exception $e) {
$attempt++;
if ($attempt <= 3) {
$backoff->sleep();
} else {
throw $e;
}
}
}
}
public function handle()
{
$backoff = new ExponentialBackoff(1000, 5); // 1s base delay, 5 retries
$attempt = 0;
while ($attempt <= 5) {
try {
$this->processIdempotentOperation();
return;
} catch (\Exception $e) {
$attempt++;
if ($attempt <= 5) {
$backoff->sleep();
}
}
}
throw new \RuntimeException('Job failed after retries');
}
Infinite Loops
$attempt or misconfiguring maxRetries can cause infinite retries.$attempt <= maxRetries and throw an exception when exceeded.Jitter (Thundering Herd)
$delay = $backoff->getDelay($attempt) + rand(0, 100);
Blocking the Event Loop
usleep() blocks the PHP process. Avoid in long-running scripts or async contexts (e.g., Laravel Horizon).sleep() for seconds or async alternatives like ReactPHP for non-blocking delays.Ignoring Non-Retryable Exceptions
$retryable = $e instanceof \Illuminate\Http\Client\ConnectionException ||
$e instanceof \PDOException && str_contains($e->getMessage(), 'deadlock');
Stateful Operations
\Log::debug("Retry attempt {$attempt}/{$maxRetries}. Waiting {$delay}ms.");
BackoffException for domain-specific errors:
throw new BackoffException('API rate limit exceeded', $attempt, $maxRetries);
baseDelay and maxRetries:
baseDelay should be ≥ 10ms (too low = thundering herd).maxRetries should align with SLA requirements (e.g., 3–5 for transient issues).Custom Strategies
Implement BackoffInterface for:
Context-Aware Retries Pass additional context to backoff logic:
class ContextualBackoff implements BackoffInterface
{
public function sleep(array $context): void
{
$delay = $this->calculateDelay($context['attempt'], $context['failureType']);
usleep($delay * 1000);
}
}
Metrics and Monitoring Track retry statistics:
$backoff = new ExponentialBackoff(100, 3);
$backoff->setOnRetry(function ($attempt, $delay) {
\App\Observers\RetryObserver::record($attempt, $delay);
});
Laravel Integration
Http::macro('retryWithBackoff', function ($url, $maxRetries = 3) {
$backoff = new ExponentialBackoff(200, $maxRetries);
// ... retry logic
});
event(new Retry
How can I help you explore Laravel packages today?