eventsauce/backoff
Small PHP library with a BackOffStrategy interface and ready-made retry delays (exponential, Fibonacci, linear). Configure initial delay, max tries, max delay, and growth base. Call backOff($tries, $throwable) inside retry loops to pause between attempts.
Installation:
composer require eventsauce/backoff
Ensure your project uses PHP 8.1+ (minimum requirement).
First Use Case:
Inject BackOffStrategy into a service handling external calls (e.g., API clients, database operations).
Example:
use EventSauce\BackOff\ExponentialBackOffStrategy;
$backOff = new ExponentialBackOffStrategy(
initialDelayMs: 100, // 100ms initial delay
maxTries: 3 // Max 3 retries
);
Where to Look First:
BackOffRunner or JitterAfterThreshold if needed.Strategy Injection:
Pass a configured BackOffStrategy to your service (e.g., via Laravel's Service Container or Dependency Injection).
// Laravel Service Provider
$this->app->bind(BackOffStrategy::class, function ($app) {
return new ExponentialBackOffStrategy(100, 5);
});
Retry Loop:
Use goto (or refactor to a method) to implement retries with back-off:
public function fetchData(ApiClient $client, BackOffStrategy $backOff): void {
$tries = 0;
start:
try {
$tries++;
$client->fetch();
} catch (RetryableException $e) {
if ($backOff->backOff($tries, $e)) {
goto start;
}
throw $e;
}
}
Laravel-Specific Patterns:
BackOffRunner for queue jobs:
use EventSauce\BackOff\BackOffRunner;
$runner = new BackOffRunner(new ExponentialBackOffStrategy(100, 3));
$runner->run(fn() => $this->processPayment());
$client = new Client([
'handler' => HandlerStack::create([
new RetryMiddleware($backOffStrategy),
// Other middleware...
]),
]);
Configuration:
Centralize back-off settings in config/backoff.php:
return [
'strategies' => [
'exponential' => [
'initial_delay_ms' => 100,
'max_tries' => 5,
'max_delay_ms' => 2500,
'base' => 2.0,
],
],
];
Bind strategies dynamically in a service provider:
$this->app->singleton(BackOffStrategy::class, function ($app) {
return new ExponentialBackOffStrategy(
$app['config']['backoff.strategies.exponential']
);
});
Infinite Retries:
maxTries: -1 enables infinite retries. Avoid in production unless explicitly required (e.g., for critical systems).Jitter Overuse:
FullJitter) adds randomness but can increase latency unpredictably.ScatteredJitter with a tight range (e.g., 0.1) for controlled randomness.Exception Handling:
backOff() returns true if retries remain; false if exhausted.if (!$backOff->backOff($tries, $e)) {
throw new MaxRetriesException("Failed after {$tries} attempts.");
}
Microseconds vs Milliseconds:
100000 = 100ms). Mistake: Passing milliseconds directly (e.g., 100) causes delays 1000x shorter.initialDelayMs: 100 with the helper methods or multiply by 1000:
new ExponentialBackOffStrategy(100 * 1000, 5) // 100ms initial delay
Thread Safety:
Log Delays: Add logging to track back-off behavior:
$backOff->backOff($tries, $e, function ($delay) {
\Log::debug("Retrying in {$delay}µs (attempt {$tries})");
});
Test Edge Cases:
max_delay_ms is respected (e.g., exponential growth capped at 2.5s).initial_delay_ms: 0 for immediate retries (rarely useful).Custom Strategies:
Implement BackOffStrategy interface:
class CustomBackOff implements BackOffStrategy {
public function backOff(int $tries, Throwable $throwable, ?callable $callback = null): bool {
$delay = $tries * 1000; // Linear but with custom logic
if ($callback) $callback($delay);
return $tries < 5;
}
}
Dynamic Jitter: Combine jitter with runtime conditions:
$jitter = new ScatteredJitter($this->getDynamicRange());
$backOff = new ExponentialBackOffStrategy(100, 5, jitter: $jitter);
Laravel Events: Dispatch events on retry exhaustion:
if (!$backOff->backOff($tries, $e)) {
event(new MaxRetriesExceeded($e));
}
Prometheus Metrics: Track retries with metrics:
$backOff->backOff($tries, $e, function ($delay) use ($metrics) {
$metrics->inc('retries_total');
$metrics->observe('retry_delay_ms', $delay / 1000);
});
How can I help you explore Laravel packages today?