retry
Re-runs a callback until it succeeds or the attempt budget runs out, sleeping in between.
retry(int|array<int, int> $times, callable $callback, \Carbon\CarbonInterval|int|\Closure $sleepMilliseconds = 0, ?callable $when = null): mixedParameters
| Parameter | Type | Description |
|---|---|---|
$$times | int|array<int, int> | Total attempts to make, or an array of sleep durations that also sets the attempt count to count($times) + 1. |
$$callback | callable | The operation to run; it receives the 1-based attempt number. |
$$sleepMillisecondsoptional | \Carbon\CarbonInterval|int|\Closure= 0 | Milliseconds to sleep between attempts, or a closure called with ($attempt, $exception) returning the duration. |
$$whenoptional | ?callable= null | Receives the thrown exception and returns whether it should be retried; a false result rethrows immediately. |
Return value
mixedWhatever the callback returns on its first successful attempt.
Errors
Rethrows the callback's last exception once the attempt budget is exhausted, or immediately when the $when closure returns false.
Changelog
- PHP 12.x$sleepMilliseconds may now be a Carbon\CarbonInterval as well as an integer or a closure; intervals are converted with totalMicroseconds.
retry() runs the callback and, if it throws, runs it again until the attempt budget is gone. The callback receives the 1-based attempt number. On success its return value is passed straight back; when the budget is exhausted the last exception is rethrown untouched.
Controlling the pause
The third argument is milliseconds. Pass a closure instead and it is called with ($attempt, $exception) so you can compute a backoff. Pass an array as the first argument and it doubles as the schedule: retry([100, 200], $cb) means three attempts in total — the array length plus one — sleeping 100 ms then 200 ms.
What trips people up
$times counts total attempts, not retries: retry(3, ...) calls the callback at most three times, not four. The array form is the exception to that rule, and that inconsistency catches people out.
Sleeping goes through Illuminate\Support\Sleep, so Sleep::fake() makes retry logic testable with no real delay and lets you assert on the durations. Finally, the $when closure decides whether an exception is retryable at all: return false and it is rethrown immediately with the remaining budget untouched.
Example
When to use it
- Re-issue an HTTP call to a flaky third-party API with exponential backoff.
- Retry a deadlocked database transaction a couple of times before failing the job.
- Wait for an eventually-consistent object store write to become readable.
Frequently asked questions
Does retry(3) mean three retries or three total attempts?
Three total attempts — the initial call plus two retries. The array form works differently: retry([100, 200], ...) allows count($times) + 1 attempts, so three attempts with two sleeps between them.
How do I add exponential backoff to retry()?
Pass a closure as the third argument; it receives the attempt number and the exception, so fn (int $attempt) => 100 * (2 ** ($attempt - 1)) gives 100ms, 200ms, 400ms. For a fixed schedule the array form retry([100, 200, 400], $callback) is simpler.
How do I stop retry() from sleeping in tests?
Call Illuminate\Support\Sleep::fake() before the code under test. retry() sleeps through the Sleep class, so faking it removes the real delay and lets you assert on the durations with Sleep::assertSleptTimes() or Sleep::assertSlept().
More examples
The four ways to describe a pause
The three-argument forms all keep the attempt count in the first argument. The array form is the odd one out: its length sets the schedule and the attempt count becomes count($array) + 1, because the last attempt has nothing to sleep before.
<?php
use Exception;
$call = fn () => $client->get($url);
// Fixed: 5 attempts, 100ms between each.
retry(5, $call, 100);
// Computed: 100ms, 200ms, 300ms, 400ms.
retry(5, $call, fn (int $attempt, Exception $e) => $attempt * 100);
// Explicit schedule: 3 attempts (2 + 1), sleeping 100ms then 200ms.
retry([100, 200], $call);
// A CarbonInterval also works on Laravel 12.
retry(3, $call, \Carbon\CarbonInterval::milliseconds(250));Retry only transient failures
The $when closure means a 422 validation error from the API is rethrown on the first attempt instead of being hammered four more times, while a dropped connection is retried with a growing backoff. Without it, every exception type consumes the whole budget.
<?php
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Throwable;
$data = retry(
5,
fn (int $attempt) => Http::timeout(5)->throw()->get('https://api.example.com/orders')->json(),
fn (int $attempt) => $attempt * 200,
fn (Throwable $e) => $e instanceof ConnectionException
);
echo count($data), " orders fetched\n";Testing retry logic without real sleeps
Sleep::fake() records durations instead of blocking, so a test covering a five-attempt backoff finishes instantly. assertSleptTimes(2) confirms the helper paused between the three attempts but not after the successful one — it needs PHPUnit, so this belongs in a test case.
<?php
use Illuminate\Support\Sleep;
Sleep::fake();
$calls = 0;
$result = retry(3, function () use (&$calls) {
$calls++;
if ($calls < 3) {
throw new RuntimeException('boom');
}
return 'ok';
}, 50);
echo $result, "\n"; // ok
echo $calls, "\n"; // 3
Sleep::assertSleptTimes(2);
Discussion