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.
Architecture fit. stechstudio/backoff is a single-purpose, framework-agnostic retry utility: given a callable and a strategy (constant, linear, exponential, polynomial, jitter), it re-invokes the callable with computed waits. That maps cleanly onto a narrow but real architectural seam — the boundary where our Laravel application calls a flaky third party (payments, shipping, CRM, SMS) or a contention-prone resource (deadlocks, optimistic-lock conflicts). It is explicitly not an orchestration layer: it does not persist state, does not survive process death, and does not know about queues. Any design that depends on retries outliving a request is a queue concern, not a library concern, and should stay in Laravel's job layer.
Integration feasibility. High and cheap. Composer install, PSR-4 autoloading, no service provider, no config file, no database migration, no published assets. The realistic integration is 20–40 lines: a container binding for a default policy, one or two named policies per integration, and a thin application-owned wrapper. The main feasibility question is not "can we" but "should we," because Laravel ships retry($times, $callback, $sleep, $when) with array-based backoff and jobs support a $backoff property plus retryUntil(). The defensible gap this package fills is strategy composition and jitter — Laravel offers linear/array backoff but not pluggable strategies (polynomial, decorrelated jitter) out of the box.
Technical risk.
retry(), this package, plus queue $backoff) will produce wildly different attempt counts for the same logical operation. Nested retries are the highest-probability defect we introduce.Key questions.
Stack fit. Compatible with a modern Laravel stack (PHP 8.1+, Laravel 9–12) as a plain Composer dependency. It introduces no framework coupling, no container magic, and no global state, so it will not conflict with Octane, queued workers, or the HTTP client. The right placement is behind an application-owned abstraction — e.g. App\Support\Retry\RetryPolicy with named policies (payments, shipping, internal-api) — so call sites express intent rather than library API.
Migration path. Do not retrofit existing retry() calls. Pick the one or two integrations where the current behavior is demonstrably inadequate (fixed sleeps causing thundering herd, or no jitter on a shared third-party API) and adopt there first. Leave Laravel's queue retry untouched: job-level retries belong to the queue, request-level retries belong to this package, and the boundary should be written down.
Compatibility. Technically clean — no shared namespaces, no conflicting globals, no autoload collisions. The compatibility work is behavioral: standardize a single retry policy per external dependency, disable HTTP-client-level retry middleware wherever this package owns the retry, and set explicit caps (maxTries, max wait, total elapsed budget) so a policy cannot exceed the request timeout. Verify the exponential strategy caps rather than grows unbounded in long-lived workers.
Sequencing.
retry() with jittered array backoff. If jitter can be approximated with random_int() in a two-line wrapper, stop and skip the dependency.Maintenance. Near-zero runtime footprint and effectively zero configuration drift. Churn risk is upstream abandonment rather than breakage — the API is small and stable. Mitigation: pin an exact version, add it to automated dependency review, and retain the option to vendor or fork (MIT permits it; the fork is a day of work, not a quarter). The real maintenance cost is the internal wrapper and the policy standard, both of which we own regardless of which library sits underneath.
Support. Community-only, no commercial backing, no SLA. Internal ownership must be explicit — a named team responsible for the wrapper, the policies, and the dependency. Do not let it become an unowned transitive dependency that appears in a security scan six months from now.
Scaling. Two constraints. First, retries are blocking: each attempt holds a PHP-FPM worker or a queue worker for the full wait duration. With exponential backoff plus jitter, a three-attempt policy can hold a worker for several seconds; under load this converts a downstream slowdown into a self-inflicted capacity incident. Enforce a total elapsed budget per policy and keep it well under the request timeout. Second, the failure mode this package addresses is correlated failure — every instance retrying in lockstep against the same degraded dependency. Jitter is the entire reason to adopt this, and it must be on by default in every policy, not left as an opt-in call-site decision.
Failure modes.
retry() multiplying attempts to 27 where the author intended 3. Prevent with an architectural rule and a lint/review check.Ramp-up. Very low. A mid-level engineer can be productive in under an hour: one wrapper, one config file, one page of documentation. The training burden is not the library — it is the policy discipline: which errors are retryable, which operations are idempotent, and which layer owns retries for a given path. Budget the onboarding effort there, and treat the library itself as an implementation detail that should remain invisible to feature teams.
Overall position. This is a competent, low-risk utility, not a platform bet. It is justified only if we have a concrete jitter/strategy requirement that Laravel's native retry cannot meet, and only if we wrap it so that replacing it later costs an afternoon. The score of 2 and thin repository metadata argue for audited, version-pinned, wrapper-mediated adoption — or, if the jitter requirement turns out to be modest, for a twenty-line internal helper and one fewer dependency.
How can I help you explore Laravel packages today?