Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Backoff Laravel Package

stechstudio/backoff

A PHP package for retrying operations with exponential backoff. Wrap any callback in the backoff helper to automatically retry failures with configurable attempts, delays, and jitter, with optional callbacks for logging or handling each attempt.

Deep Wiki
Context7

Getting Started

Install with Composer; the package is framework-agnostic but drops cleanly into Laravel:

composer require stechstudio/backoff

The entry point is STS\Backoff\Backoff. The smallest useful example wraps a flaky call and retries it with a backoff curve:

use STS\Backoff\Backoff;

$result = (new Backoff('exponential'))->run(function () use ($client) {
    return $client->get('https://api.example.com/things');
});

run() returns whatever the callback returns and throws the last exception once attempts are exhausted — so for a first integration you only need to wrap the call site and move on.

Where to look first:

  1. The README for the config array shape and the list of strategy names.
  2. src/Backoff.php — the whole public surface (run, setStrategy, setMaxAttempts, setWaitCap, setWaitTime, setJitter, setExceptionHandler, setFallback) and the defaults. Read these defaults before shipping anything to production.
  3. src/Strategies/ — one class per curve (Constant, Linear, Exponential, Polynomial, Fibonacci, plus jitter handling). This is the best reference for how the wait time is calculated per attempt.

First concrete use case: wrap a single outbound HTTP/SDK call that occasionally returns 5xx or times out, with exponential + a waitCap, and a setExceptionHandler that logs each failure. That gives you the 80% benefit with about ten lines.

Implementation Patterns

Config array instead of constructor string. For anything beyond a toy, construct with an array so the policy is self-documenting:

$backoff = new Backoff([
    'strategy'    => 'exponential',
    'maxAttempts' => 5,
    'waitTime'    => 100,    // base delay, milliseconds
    'waitCap'     => 10000,  // ceiling, milliseconds
]);

Use the fluent setters (setStrategy, setMaxAttempts, setWaitCap) when the policy is decided at runtime.

Pick the strategy to match the failure mode.

  • constant — fixed delay; good for rate-limit-ish retries where the dependency recovers on a known cadence.
  • linear / polynomial — gentle growth; good for internal services that are usually healthy.
  • exponential — the default choice for external APIs and anything that can amplify load during an outage.
  • fibonacci — exponential-like growth with less aggressive early waits; useful when you want quick first retries but a slow ceiling.
  • jitter (enabled via config/setter) — always turn this on when many processes may retry the same failing dependency. Without it, retries synchronize and hammer the target.

Custom strategy via closure. If none of the built-ins fit, pass a callable that receives the attempt number and returns milliseconds:

$backoff->setStrategy(fn (int $attempt) => min(100 * 2 ** $attempt, 5000));

This is the cleanest extension point and avoids writing a custom strategy class.

Log per-attempt failures with setExceptionHandler. It fires on each caught exception before the next wait, which is where you want structured logging or metrics — the final exception thrown by run() is the only one you'd otherwise see:

$backoff->setExceptionHandler(function (\Throwable $e, int $attempt) use ($logger) {
    $logger->warning('retrying', ['attempt' => $attempt, 'error' => $e->getMessage()]);
});

Graceful degradation with setFallback. When every attempt fails, the fallback is invoked instead of rethrowing, and its return value becomes run()'s return value. Use it for cache warming, default responses, or queueing the work for later:

$backoff->setFallback(fn (\Throwable $e) => Cache::get('last_known_good'));

Laravel integration. Bind a factory in a service provider so call sites don't re-specify policy:

$this->app->bind('backoff.external', fn () => new Backoff([
    'strategy' => 'exponential', 'maxAttempts' => 5, 'waitCap' => 10000,
]));

Then wrap Http:: calls or Guzzle handlers. For queued work, prefer the job's own retry semantics ($tries, backoff()) over in-process sleeping; use this package for the outbound dependency inside the job, with a small waitCap.

Keep the closure narrow. The retried unit should be one HTTP call or one SDK call, not a multi-step transaction — otherwise you risk partially-applied side effects on retry.

Gotchas and Tips

  • It blocks. The implementation sleeps between attempts (usleep-style). A waitCap of 30s across five attempts can park a PHP-FPM worker for over a minute. In web requests keep caps small; in long jobs, understand you're consuming a worker.
  • maxAttempts includes the first try. Setting it to 1 disables retrying and is the easiest way to make tests fast and deterministic.
  • Units are milliseconds. waitTime and waitCap are ms, not seconds — a waitCap of 30 is 30ms, not 30s.
  • Exception type coverage. Verify which throwable type is caught (often \Exception, not \Throwable). A TypeError or other \Error will bypass retry entirely; if your wrapped code can throw those, catch and rethrow as Exception inside the closure or add your own guard.
  • Falsy return values. Confirm against the source whether a falsy return short-circuits or counts as failure — don't rely on return false to signal failure unless you've verified it in the version you pinned.
  • Only retry idempotent operations. Retrying a POST that may have succeeded on the server is a data-corruption bug, not a resilience feature. Use idempotency keys or restrict retries to GET/HEAD/known-safe calls.
  • Jitter is not optional at scale. If dozens of workers can hit the same dependency, enable jitter even if you keep the rest of the policy simple.
  • Testing. Inject a factory or bind a fake in the container so tests can set maxAttempts(1); otherwise your test suite inherits real sleeps. Assert on the exception handler and fallback rather than on wall-clock timing.
  • Extension points. Three are worth knowing: a custom strategy closure, a custom strategy class under Strategies/ (mirror an existing one), and the exceptionHandler/fallback hooks for cross-cutting behavior like metrics and degradation. There's no built-in circuit breaker — if you need "stop trying entirely for N minutes," layer that on top yourself.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib