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

alt-three/retry

alt-three/retry is a Laravel package for automatically retrying failed operations and queued jobs with configurable attempts and delays. It helps PHP applications handle transient errors gracefully and integrate retry logic cleanly into Laravel workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Pin it and check compatibility first. This package is archived and last tagged in September 2018, so it targets Laravel 5.x-era contracts (Illuminate\Bus\Dispatcher, Illuminate\Contracts\Bus\Dispatcher) and PHP 7.0–7.2. Add it with an explicit constraint (composer require "alt-three/retry:^3.0") rather than *, and run composer why-not against your framework version before writing any code.
  • Look at two files first: vendor/alt-three/retry/src/RetryServiceProvider.php (how the middleware is registered on the dispatcher) and vendor/alt-three/retry/src/RetryMiddleware.php (the actual retry loop, attempt counting, and exception handling). The README is short; the middleware source is the real documentation for what "retry" means here.
  • Register the provider the Laravel 5 way, in config/app.php:
'providers' => [
    // ...
    AltThree\Retry\RetryServiceProvider::class,
],
  • First use case: a command that hits a flaky external dependency and is dispatched synchronously (or from a queue job) where an immediate second attempt is likely to succeed — e.g. an HTTP call, a Redis lock acquisition, or a MySQL deadlock on a write. The middleware gives you an in-process retry without releasing the job back onto the queue.
  • Confirm the contract before relying on it. Check whether the package expects a marker interface/trait on your command (something like a Retryable command with an attempt/retry count accessor) or whether it retries everything on the bus. If it retries everything, you must scope it deliberately — see below.

Implementation Patterns

1. Global middleware on the dispatcher. The provider typically pushes the middleware into the bus pipeline. If you want it explicit (or want it only in some contexts), register it yourself in a service provider:

public function boot(Dispatcher $dispatcher)
{
    $dispatcher->pipeThrough([
        AltThree\Retry\RetryMiddleware::class,
    ]);
}

2. Opt-in retryable commands. Keep the blast radius small — only commands that are safe to run twice should go through the retrying dispatcher. If the package exposes a retryable marker, implement it per command; otherwise dispatch "retryable" work through a dedicated dispatcher instance and leave the default bus alone.

class SyncInvoiceToErp extends Command
{
    use DispatchesJobs;

    public $attempts = 0;

    public function handle(ErpClient $erp)
    {
        $this->attempts++;

        $erp->push($this->invoice); // throws on transient failure
    }
}

3. Layer it correctly with the queue. These are two different retry mechanisms:

Mechanism Where Behaviour
alt-three/retry middleware Inside one job execution / sync dispatch Immediate re-run, worker held
Laravel $tries / --tries Queue worker Job released back to queue with delay

Use the middleware for micro-retries (fast transient blips, 1–3 attempts with a short sleep) and Laravel's $tries/backoff() for macro-retries (minutes-to-hours, surviving worker restarts). Don't crank both.

4. Add jitter/backoff inside the command. The middleware usually just re-invokes; if there's no delay configuration, put the sleep in your handler so you don't hammer a struggling service:

public function handle(ErpClient $erp)
{
    try {
        $erp->push($this->invoice);
    } catch (ConnectionException $e) {
        usleep(random_int(100_000, 500_000) * ($this->attempts + 1));
        throw $e; // let the middleware decide whether to retry
    }
}

5. Wrap only genuinely transient work. The safest integration is at the edge of your domain: a small RetryableAction/service class that owns the external call, called from commands and jobs alike. That keeps the middleware a bus concern and your domain logic unaware of it.

6. Observability. Log every attempt with a correlation ID (job ID, command class, attempt number). If you're already in the Alt Three ecosystem, pipe it through alt-three/logger; otherwise a plain Log::warning('retry', [...]) in the handler is enough to debug later.

7. Testing. Use Bus::fake() to assert dispatch, and unit-test the middleware/handler interaction directly with a command that throws N times then succeeds. Prefer a fake over an integration test — a real retry loop with sleeps makes the suite slow and flaky.

Gotchas and Tips

  • Archived = frozen. No security patches, no Laravel 6/7/8 compatibility work. If you're modernising, plan a migration path: Laravel's retry() helper (5.6+), Illuminate\Support\Retry (5.8+), or queue backoff()/$tries covers nearly all of what this package does. Treat adoption as a short-term bridge.
  • Multiplicative retries. Middleware retries × queue $tries × worker restarts can turn "3 retries" into dozens of executions. Do the arithmetic explicitly and cap the total.
  • Idempotency is on you. A command that partially completed before throwing will run again from the top. Guard writes with unique constraints, upserts, idempotency keys, or an explicit "already done" state check — the middleware cannot know what your command already did.
  • Long sleeps block the worker. In-process retries hold a queue worker slot. If your retry delay is measured in seconds, you're burning worker time; prefer releasing the job with backoff() beyond ~1s.
  • Worker timeout. Cumulative retry time must stay well under --timeout, or the worker is SIGKILLed mid-retry and the job is retried again at the queue layer — silently doubling attempts.
  • Catch specific exceptions. If your handler swallows Throwable or catches Exception broadly, you'll retry ValidationException/ModelNotFoundException forever-ish. Let non-transient failures bubble immediately.
  • Mutated state survives retries. Instance properties on the command (counters, collected results, open handles) persist across attempts because the same object is re-invoked. Reset per-attempt state at the top of handle().
  • Dispatcher is a singleton. Swapping the dispatcher (e.g. in tests or a custom Bus binding) after the provider has piped middleware means your retry middleware disappears. Register middleware in boot() against the resolved instance, and be explicit about which dispatcher you're touching.
  • Config caching. If you cache config, provider registration changes require php artisan config:clear; a missing provider after deploy is almost always a stale bootstrap/cache/config.php.
  • Extension point: rather than editing vendor code, wrap RetryMiddleware with your own middleware that logs, enforces a per-command allow/deny list, and delegates to the packaged class — that keeps you upgradable (or migratable) if you ever swap the package out.
  • Abandoned-dependency hygiene: because there are zero dependents and no recent releases, you're the only one testing this against your framework version. Pin the exact version, and add a single integration test that exercises a failing-then-succeeding command so a future composer update breaking it is caught in CI.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
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