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

Retry Laravel Package

vkartaviy/retry

A Laravel retry helper that re-runs failed operations with configurable attempts, delays, and backoff. Wrap closures or jobs to handle transient errors gracefully, with support for conditional retries and custom exception handling.

Deep Wiki
Context7

Getting Started

Install and verify first. The package is zero-dependency but last released in 2015, so the very first step is a compatibility check before you build anything on it:

composer require vkartaviy/retry
php -r "require 'vendor/autoload.php'; var_dump(class_exists('Vkartaviy\\Retry\\Retry'));"

Then open vendor/vkartaviy/retry/src/ and read the main class plus its tests — at 29 stars and a decade old, the source is the only reliable documentation. Confirm three things before writing production code: the exact builder method names, the delay unit (milliseconds vs. microseconds passed to usleep), and whether failures are typed against Exception or Throwable.

Where to look first. The tests directory is effectively the usage manual — copy a working chain from there rather than from the README, which may describe an API that drifted.

First use case. Wrap a flaky third-party HTTP call:

use Vkartaviy\Retry\Retry;

$response = Retry::make()
    ->attempts(3)
    ->delay(100)          // verify: ms or µs
    ->multiplier(2)       // 100, 200, 400
    ->jitter(true)
    ->retryIf(fn ($e) => $e instanceof \GuzzleHttp\Exception\ConnectException)
    ->onAttempt(fn ($attempt, $e) => Log::warning('retry', ['attempt' => $attempt]))
    ->run(fn () => $client->get('https://api.example.com/prices'));

If no global retry() helper ships with the package, wrap the builder in your own small helper class — do not rely on the function existing.

Implementation Patterns

Centralise the policy, not the call site. Inline ->attempts(5) scattered across a codebase is unmaintainable. Put policies in config/retries.php keyed by downstream service, and have a thin RetryFactory (container-bound) build a configured instance:

// config/retries.php
'payments' => ['attempts' => 4, 'delay' => 200, 'multiplier' => 2, 'jitter' => true],

This also gives you a single place to zero out delays in tests.

Only retry idempotent work. GET/PUT/DELETE and read queries are safe; a POST that charges a card is not. If you must retry a write, add an idempotency key at the HTTP layer or a dedupe guard at the DB layer — the package has no concept of idempotency.

Database deadlocks: open the transaction inside the closure. This is the single most common mistake:

Retry::make()->attempts(3)->retryIf(fn ($e) => str_contains($e->getMessage(), 'Deadlock'))
    ->run(function () {
        DB::transaction(function () { /* the actual work */ });  // fresh txn per attempt
    });

Wrapping the retry outside an already-open DB::transaction() retries inside a poisoned transaction and fails identically every time.

Queue jobs: retry the dependency, not the job. Use the package inside handle() around the external call, and let Laravel's $tries/$backoff handle job-level retries. Stacking 4 attempts × 3 queue tries × Guzzle's retry middleware gives 36 requests to a service that is already down — retry amplification is how you turn an outage into an incident.

Exception filtering is the real feature. Retry ConnectException, HTTP 429/5xx, deadlocks, and lock timeouts. Never retry 400/401/422, ModelNotFoundException, or unique-constraint violations — they are deterministic and retrying just burns your latency budget.

Callbacks for observability. Use onAttempt to log attempt number and elapsed time (metrics on retry rate is how you discover a degrading dependency before it pages you), and the final-failure callback to report to Sentry. Confirm the last exception is still rethrown after the callback runs — swallowing it silently is the worst possible failure mode.

Testing. Set delay/attempts from config so the test environment uses attempts = 1 or zero delay; otherwise your suite really sleeps and CI crawls. Assert on a counter closure incrementing per invocation to prove the retry actually happened.

Gotchas and Tips

  • Name collision with Laravel's built-in retry(). Laravel has shipped retry($times, $callback, $sleep, $when) for years. If the package defines a global helper, you get a fatal redeclare or a confusing shadow. Prefer the namespaced class, or alias explicitly. For simple fixed-delay cases, Laravel's native helper is usually the better answer; reach for this package only when you need jitter and per-attempt callbacks.
  • No Throwable support (2015 codebase). PHP 7+ TypeError, Error, and most \RuntimeException subclasses typed as Throwable may fall straight through. Test it, and if broken, subclass or wrap the builder rather than patching vendor/.
  • Delay unit off-by-1000. A 100 intended as milliseconds becoming 100 µs makes retries instantaneous and useless; the reverse hangs a worker for 100 seconds. Assert this against the source, then write one test that measures wall-clock duration.
  • No max-delay cap. Multiplier 2 over 8 attempts is a 128× base delay. In PHP-FPM or a synchronous queue worker, a retry ladder longer than your request timeout produces a 504 with no useful log — budget total sleep against max_execution_time.
  • Builder reuse. If the fluent object is stateful and you resolve it as a singleton, attempt counters or accumulated callbacks can leak between jobs in a long-running worker. Build a fresh instance per call site.
  • Verify jitter behaviour. "Jitter" sometimes means full replacement of the delay rather than a fraction of it. Check the distribution — bad jitter can collapse backoff to near-zero and hammer the failing service.
  • Pin and isolate. The package is effectively unmaintained. Pin the exact version in composer.json and put it behind your own Retrier interface so that swapping to symfony/retry or Laravel's native helper later is a one-file change instead of a grep-and-replace.
  • Debugging tip. Temporarily log inside the sleeper (or set jitter off and multiplier 1) to make the backoff sequence observable; silent exponential waits are frequently misdiagnosed as a hung request.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin