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.
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.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.config/app.php:'providers' => [
// ...
AltThree\Retry\RetryServiceProvider::class,
],
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.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.
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.$tries × worker restarts can turn "3 retries" into dozens of executions. Do the arithmetic explicitly and cap the total.backoff() beyond ~1s.--timeout, or the worker is SIGKILLed mid-retry and the job is retried again at the queue layer — silently doubling attempts.Throwable or catches Exception broadly, you'll retry ValidationException/ModelNotFoundException forever-ish. Let non-transient failures bubble immediately.handle().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.php artisan config:clear; a missing provider after deploy is almost always a stale bootstrap/cache/config.php.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.composer update breaking it is caught in CI.How can I help you explore Laravel packages today?