PHPackages                             yannelli/attempt - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. yannelli/attempt

ActiveLibrary[Debugging &amp; Profiling](/categories/debugging)

yannelli/attempt
================

A fluent attempt/retry/fallback system for Laravel

v1.3.0(2w ago)441.3k↓70.7%[3 PRs](https://github.com/yannelli/attempt/pulls)MITPHPPHP ^8.4CI passing

Since Jan 19Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/yannelli/attempt)[ Packagist](https://packagist.org/packages/yannelli/attempt)[ Docs](https://github.com/yannelli/attempt)[ GitHub Sponsors](https://github.com/yannelli)[ RSS](/packages/yannelli-attempt/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (7)Dependencies (47)Versions (14)Used By (0)

Attempt
=======

[](#attempt)

 [![Attempt — Retry. Fallback. Recover.](art/banner.png)](art/banner.png)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7480c4124593b31a50e6527310ef6f8e0f2f632b87ce35d9b148cafd12806a33/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f79616e6e656c6c692f617474656d70742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/yannelli/attempt)[![GitHub Tests Action Status](https://camo.githubusercontent.com/0075b3714786bdc17cbe5fae948f9020fe9434a13b076824dd53d9badca2c79c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f79616e6e656c6c692f617474656d70742f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/yannelli/attempt/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/4675c8b9caaf3d162d9d5627c18de1110b1fd90fb36d3fa2ece967bed12843a4/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f79616e6e656c6c692f617474656d70742e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/yannelli/attempt)

- [Introduction](#introduction)
- [Installation](#installation)
- [Making Attempts](#making-attempts)
    - [Basic Usage](#basic-usage)
    - [Attemptable Classes](#attemptable-classes)
    - [Self-Configuring Classes](#self-configuring-classes)
- [Retry Configuration](#retry-configuration)
    - [Specifying Retry Attempts](#specifying-retry-attempts)
    - [Delay Strategies](#delay-strategies)
    - [Conditional Retries](#conditional-retries)
- [Fallback Handlers](#fallback-handlers)
    - [Defining Fallbacks](#defining-fallbacks)
    - [Fallback Chains](#fallback-chains)
    - [The Fallbackable Interface](#the-fallbackable-interface)
- [Exception Handling](#exception-handling)
    - [Catching Exceptions](#catching-exceptions)
    - [Re-throwing Exceptions](#re-throwing-exceptions)
    - [Suppressing Exceptions](#suppressing-exceptions)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Conditional Execution](#conditional-execution)
- [Pipeline Integration](#pipeline-integration)
    - [Pipeline Attempts](#pipeline-attempts)
    - [Using AttemptPipe](#using-attemptpipe)
- [Concurrent Execution](#concurrent-execution)
    - [Running Concurrent Attempts](#running-concurrent-attempts)
    - [Racing Attempts](#racing-attempts)
- [Async Execution](#async-execution)
- [Laravel AI Integration](#laravel-ai-integration)
    - [Retrying AI Requests](#retrying-ai-requests)
    - [Per-Provider Retries with Agent Middleware](#per-provider-retries-with-agent-middleware)
    - [Retry Safety](#retry-safety)
- [Working with Results](#working-with-results)
    - [The AttemptResult Object](#the-attemptresult-object)
    - [Monadic Operations](#monadic-operations)
- [Events](#events)
- [Testing](#testing)
- [Configuration](#configuration)

Introduction
------------

[](#introduction)

While building your application, you may encounter operations that can fail due to transient issues like network timeouts, API rate limits, or temporary service unavailability. Rather than letting these failures crash your application or writing repetitive try-catch blocks, Laravel Attempt provides a fluent, composable system for handling retries, fallbacks, and error recovery.

Attempt treats error handling as a first-class pipeline concern, allowing you to declaratively define how your application should respond when things go wrong. Whether you need simple retry logic with exponential backoff, complex fallback chains, or integration with Laravel’s native Pipeline, Attempt provides an expressive API that reads like natural language.

Installation
------------

[](#installation)

Attempt supports PHP 8.4 or newer and Laravel 12 or 13.

You may install Attempt into your project using the Composer package manager:

```
composer require yannelli/attempt
```

After installing Attempt, you may optionally publish its configuration file using the `vendor:publish` Artisan command:

```
php artisan vendor:publish --tag="attempt-config"
```

Making Attempts
---------------

[](#making-attempts)

### Basic Usage

[](#basic-usage)

The simplest way to use Attempt is to wrap a potentially failing operation with the `try` method. To execute the attempt and retrieve the result, you may call the `thenReturn` method:

```
use Yannelli\Attempt\Facades\Attempt;

$result = Attempt::try(fn() => $api->call())->thenReturn();
```

If you need to pass input to your callable, you may provide additional arguments to the `try` method:

```
$result = Attempt::try(MyAction::class, $order, $user)->thenReturn();
```

You may also pass an array of callables to the `try` method. When an array is provided, each callable will be executed in order as a fallback chain. If the first callable fails, the second will be attempted, and so on:

```
$result = Attempt::try([
    PrimaryProvider::class,
    BackupProvider::class,
    CachedResponse::class,
], $payload)->thenReturn();
```

Attempt provides several methods for executing your attempt and retrieving the result:

MethodBehavior`then(Closure $callback)`Transform and return the final result`thenReturn()`Return the processed value directly`thenReturnOrFail()`Return the value or throw on failure`run()`Return an `AttemptResult` object with metadata`get()`Alias for `thenReturn()``value()`Alias for `thenReturn()`### Attemptable Classes

[](#attemptable-classes)

For more complex operations, you may create dedicated attemptable classes. These classes should implement the `Attemptable` interface and define a `handle` method that receives the input and returns a result:

```
use Yannelli\Attempt\Contracts\Attemptable;

class FetchUserData implements Attemptable
{
    public function handle(mixed ...$input): mixed
    {
        [$userId] = $input;

        return Http::get("https://api.example.com/users/{$userId}")->json();
    }
}
```

Once you have defined your attemptable class, you may pass its class name to the `try` method:

```
$userData = Attempt::try(FetchUserData::class, $userId)
    ->retry(3)
    ->thenReturn();
```

### Self-Configuring Classes

[](#self-configuring-classes)

Sometimes you may want a class to define its own retry and fallback configuration. To accomplish this, your class may implement both the `Attemptable` and `ConfiguresAttempt` interfaces. The `configureAttempt` method receives an `AttemptBuilder` instance that you may use to define your preferred configuration:

```
use Yannelli\Attempt\Contracts\Attemptable;
use Yannelli\Attempt\Contracts\ConfiguresAttempt;
use Yannelli\Attempt\AttemptBuilder;

class ResilientApiCall implements Attemptable, ConfiguresAttempt
{
    public function configureAttempt(AttemptBuilder $attempt): void
    {
        $attempt
            ->retry(3)
            ->exponentialBackoff(base: 100, max: 5000)
            ->withJitter(0.1);
    }

    public function handle(mixed ...$input): mixed
    {
        return Http::get('https://api.example.com/data')->json();
    }
}
```

When using a self-configuring class, the configuration is automatically applied:

```
$result = Attempt::try(ResilientApiCall::class)->thenReturn();
```

Retry Configuration
-------------------

[](#retry-configuration)

### Specifying Retry Attempts

[](#specifying-retry-attempts)

By default, Attempt will not retry a failed operation. To enable retries, call the `retry` method and specify how many retries are allowed in addition to the initial attempt. For example, `retry(3)` permits up to four total executions:

```
Attempt::try($callable)
    ->retry(3)
    ->thenReturn();
```

### Delay Strategies

[](#delay-strategies)

Often, you will want to wait between retry attempts to give transient issues time to resolve. Attempt provides several strategies for configuring delays between retries.

#### Fixed Delay

[](#fixed-delay)

To wait a fixed number of milliseconds between all retries, pass an integer to the `delay` method:

```
Attempt::try($callable)
    ->retry(3)
    ->delay(100) // Wait 100ms between retries
    ->thenReturn();
```

#### Explicit Delays

[](#explicit-delays)

If you need different delays for each retry attempt, you may pass an array of millisecond values:

```
Attempt::try($callable)
    ->retry(3)
    ->delay([1000, 5000, 15000]) // 1s, 5s, 15s
    ->thenReturn();
```

#### Exponential Backoff

[](#exponential-backoff)

Exponential backoff progressively increases the delay between retries. This strategy is particularly useful when interacting with rate-limited APIs or overloaded services. The `exponentialBackoff` method accepts a base delay and an optional maximum delay:

```
Attempt::try($callable)
    ->retry(5)
    ->exponentialBackoff(base: 100, max: 30000) // 100ms, 200ms, 400ms, 800ms...
    ->thenReturn();
```

#### Linear Backoff

[](#linear-backoff)

Linear backoff increases the delay by a fixed increment with each retry:

```
Attempt::try($callable)
    ->retry(3)
    ->linearBackoff(base: 100, increment: 100) // 100ms, 200ms, 300ms
    ->thenReturn();
```

#### Adding Jitter

[](#adding-jitter)

To prevent multiple failing operations from retrying in lockstep (known as the “thundering herd” problem), you may add randomized jitter to your delays. The `withJitter` method accepts a percentage value that determines how much variance to apply:

```
Attempt::try($callable)
    ->retry(3)
    ->delay([1000, 5000, 10000])
    ->withJitter(0.2) // +/- 20% randomization
    ->thenReturn();
```

#### Custom Delay Functions

[](#custom-delay-functions)

For complete control over delay calculation, you may use the `delayUsing` method with a closure that receives the current attempt number and the exception that triggered the retry:

```
Attempt::try($callable)
    ->retry(5)
    ->delayUsing(fn(int $attempt, ?Throwable $e) => $attempt * 1000)
    ->thenReturn();
```

### Conditional Retries

[](#conditional-retries)

Sometimes you may only want to retry an operation for specific types of failures. The `retryIf` method accepts a closure that receives the thrown exception and returns a boolean indicating whether the operation should be retried:

```
Attempt::try($callable)
    ->retry(3)
    ->retryIf(fn(Throwable $e) => $e instanceof ConnectionException)
    ->thenReturn();
```

Fallback Handlers
-----------------

[](#fallback-handlers)

### Defining Fallbacks

[](#defining-fallbacks)

When an operation fails after exhausting all retries, you may want to execute a fallback operation instead of throwing an exception. Use the `fallback` method to define an alternative callable:

```
Attempt::try(PrimaryApi::class)
    ->fallback(BackupApi::class)
    ->thenReturn();
```

### Fallback Chains

[](#fallback-chains)

You may define multiple fallbacks that will be tried in order. The first successful fallback wins:

```
Attempt::try(PrimaryApi::class)
    ->fallback([
        SecondaryApi::class,
        TertiaryApi::class,
        fn() => Cache::get('fallback_value'),
    ])
    ->thenReturn();
```

For a more expressive syntax, you may chain multiple `orFallback` calls:

```
Attempt::try(PrimaryApi::class)
    ->orFallback(SecondaryApi::class)
    ->orFallback(fn() => 'default')
    ->thenReturn();
```

### The Fallbackable Interface

[](#the-fallbackable-interface)

For fallback classes that need access to the original exception, implement the `Fallbackable` interface. This interface defines a `handleFallback` method that receives both the exception and the original input:

```
use Yannelli\Attempt\Contracts\Fallbackable;

class ApiErrorFallback implements Fallbackable
{
    public function handleFallback(Throwable $e, mixed ...$input): mixed
    {
        Log::warning('Using fallback due to: ' . $e->getMessage());

        return Cache::get('cached_response');
    }

    public function shouldSkip(Throwable $e): bool
    {
        // Skip this fallback for certain exceptions
        return $e instanceof ValidationException;
    }
}
```

Exception Handling
------------------

[](#exception-handling)

### Catching Exceptions

[](#catching-exceptions)

Attempt allows you to register exception handlers that will be invoked when specific exceptions occur. You may catch specific exception types or all exceptions:

```
// Catch specific exceptions
Attempt::try($callable)
    ->catch(ConnectionException::class, fn($e) => Log::error($e))
    ->catch(TimeoutException::class, fn($e) => Metrics::timeout())
    ->thenReturn();

// Catch all exceptions
Attempt::try($callable)
    ->catch(fn(Throwable $e) => Log::error($e))
    ->thenReturn();
```

### Re-throwing Exceptions

[](#re-throwing-exceptions)

If you want to execute a handler but still throw the exception afterward, chain the `throw` method:

```
Attempt::try($callable)
    ->catch(fn($e) => Log::error($e))
    ->throw()
    ->thenReturn();
```

### Suppressing Exceptions

[](#suppressing-exceptions)

To suppress all exceptions and return `null` on failure, use the `quiet` method:

```
Attempt::try($callable)
    ->quiet()
    ->thenReturn(); // Returns null on failure
```

Lifecycle Hooks
---------------

[](#lifecycle-hooks)

Attempt provides several hooks that allow you to execute code at specific points during the attempt lifecycle:

```
Attempt::try($callable)
    ->finally(fn($context) => Log::info('Attempt completed'))
    ->defer(fn($context) => Metrics::record($context->elapsed()))
    ->onRetry(fn($context, $e) => Log::warning("Retry {$context->attemptNumber}"))
    ->onSuccess(fn($context, $result) => Cache::put('last_result', $result))
    ->onFailure(fn($context, $e) => Alert::send($e))
    ->thenReturn();
```

Conditional Execution
---------------------

[](#conditional-execution)

You may conditionally execute an attempt using the `when` and `unless` methods:

```
// Only execute if condition is true
Attempt::try($callable)
    ->when($shouldRun)
    ->thenReturn();

// Only execute if condition is false
Attempt::try($callable)
    ->unless($shouldSkip)
    ->thenReturn();

// With closure conditions
Attempt::try($callable)
    ->when(fn() => Feature::active('new-api'))
    ->thenReturn();
```

Pipeline Integration
--------------------

[](#pipeline-integration)

### Pipeline Attempts

[](#pipeline-attempts)

Attempt integrates seamlessly with Laravel’s Pipeline. Use the `pipeline` method to execute a series of stages with built-in retry and fallback capabilities:

```
$result = Attempt::pipeline([
    ValidateInput::class,
    ProcessData::class,
    SaveToDatabase::class,
])
    ->send($data)
    ->retry(2)
    ->thenReturn();
```

### Using AttemptPipe

[](#using-attemptpipe)

You may also use `AttemptPipe` within a native Laravel Pipeline to wrap individual stages with retry logic:

```
use Illuminate\Support\Facades\Pipeline;
use Yannelli\Attempt\Pipes\AttemptPipe;

$result = Pipeline::send($data)
    ->through([
        AttemptPipe::wrap(ExternalApiCall::class)
            ->retry(3)
            ->delay([100, 500, 1000]),
        ProcessResponse::class,
    ])
    ->thenReturn();
```

Concurrent Execution
--------------------

[](#concurrent-execution)

### Running Concurrent Attempts

[](#running-concurrent-attempts)

Use the `concurrent` method to execute a group of independent attempts and receive an array of results. Attempts currently execute in declaration order in the same process; this API groups results but does not provide parallel execution:

```
$concurrent = Attempt::concurrent([
    fn() => Http::get('https://api1.example.com'),
    fn() => Http::get('https://api2.example.com'),
    fn() => Http::get('https://api3.example.com'),
]);

// Run all and get array of AttemptResult objects
$results = $concurrent->run();

// Get only successful results
$successful = Attempt::concurrent([...])->successful();

// Get only failed results
$failed = Attempt::concurrent([...])->failed();

// Get values directly
$values = Attempt::concurrent([...])->thenReturn();
```

### Racing Attempts

[](#racing-attempts)

Use the `race` method to try operations in declaration order until one succeeds. Remaining operations are skipped after the first success:

```
$result = Attempt::race([
    PrimaryProvider::class,
    SecondaryProvider::class,
    TertiaryProvider::class,
])->thenReturn();
```

Async Execution
---------------

[](#async-execution)

For long-running operations, you may dispatch an attempt to run asynchronously on the queue:

```
Attempt::try(LongRunningTask::class, $data)
    ->retry(3)
    ->async()
    ->onQueue('processing')
    ->then(fn ($value) => Log::info('Completed', ['value' => $value]))
    ->catch(fn (Throwable $e) => Log::error('Failed', ['error' => $e->getMessage()]))
    ->dispatch();
```

If you need to execute the attempt synchronously instead (bypassing the queue), you may use the `await` method:

```
$result = Attempt::try(LongRunningTask::class, $data)
    ->retry(3)
    ->async()
    ->await(); // Runs synchronously, returns AttemptResult
```

Laravel AI Integration
----------------------

[](#laravel-ai-integration)

Attempt provides first-class integration with the official [Laravel AI SDK](https://github.com/laravel/ai). While the SDK offers provider failover out of the box, it does not retry failed requests. Attempt fills this gap with retry policies tuned specifically for AI workloads, giving you three composable layers of resilience: retry each provider with backoff, fail over across providers, and finally fall back to a cached or canned value.

To get started, install the Laravel AI SDK alongside Attempt:

```
composer require laravel/ai
```

### Retrying AI Requests

[](#retrying-ai-requests)

The `ai` method creates an attempt that is pre-configured for AI requests. Only transient failures will be retried: rate limits, provider overloads, connection errors, and retryable HTTP statuses (408, 429, and 5xx). Permanent failures such as insufficient credits, unknown tools, invalid requests, and malformed responses fail immediately:

```
use Yannelli\Attempt\Facades\Attempt;

$response = Attempt::ai(fn () => SupportAgent::make()->prompt($question))
    ->retry(3)
    ->thenReturn();
```

When a provider supplies a `Retry-After` header with a rate limit response, Attempt will honor it (capped at 30 seconds). Otherwise, delays are calculated using decorrelated jitter starting at 500 milliseconds. You may override the delay behavior using any of the standard delay methods:

```
Attempt::ai(fn () => SupportAgent::make()->prompt($question))
    ->retry(3)
    ->delayUsing(fn (int $attempt) => $attempt * 1000)
    ->thenReturn();
```

Because `ai` returns a standard attempt builder, the full Attempt API remains available. For example, you may combine AI retries with the SDK's provider failover and a non-AI fallback:

```
$response = Attempt::ai(fn () => SupportAgent::make()->prompt(
    $question,
    provider: ['openai-primary', 'anthropic-backup'],
))
    ->retry(2)
    ->fallback(fn () => CannedResponse::for($question))
    ->thenReturn();
```

The `ai` method works equally well for the SDK's other operations, such as image generation and embeddings, since they throw the same transient exception types:

```
use Laravel\Ai\Embeddings;

$embeddings = Attempt::ai(fn () => Embeddings::for($chunks)->generate())
    ->retry(3)
    ->thenReturn();
```

If you need to customize retry classification, the underlying policy is exposed as `Yannelli\Attempt\Ai\AiRetryPolicy`, and the standard `retryIf` method may be used to replace it entirely.

### Per-Provider Retries with Agent Middleware

[](#per-provider-retries-with-agent-middleware)

The SDK's provider failover moves to the next provider on the first failure. If you would rather retry each provider before failing over, add the `RetryAiRequests` middleware to your agent. Since agent middleware runs once per provider in the failover list, each provider will be retried independently:

```
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Promptable;
use Yannelli\Attempt\Ai\RetryAiRequests;

class SupportAgent implements Agent, HasMiddleware
{
    use Promptable;

    public function instructions(): string
    {
        return 'Answer questions about our product accurately and concisely.';
    }

    public function middleware(): array
    {
        return [
            RetryAiRequests::times(2),
        ];
    }
}
```

After exhausting its retries, the middleware re-throws the original exception so the SDK's failover proceeds normally. You may customize the underlying attempt using the `configureUsing` method:

```
RetryAiRequests::times(2)->configureUsing(
    fn ($attempt) => $attempt
        ->exponentialBackoff(base: 250, max: 10000)
        ->onRetry(fn ($context, $e) => Log::warning('Retrying AI request', [
            'attempt' => $context->attemptNumber,
        ]))
);
```

### Retry Safety

[](#retry-safety)

A few caveats apply when retrying AI operations:

- **Streaming**: streamed responses are lazy, so failures that occur while iterating a stream happen outside the attempt and will not be retried. Never re-issue a streamed request after output has reached the user, as this may duplicate or change previously delivered content.
- **Tool-using agents**: a failed prompt may have already executed tools during earlier generation steps. Retrying the prompt will run those tools again, so ensure your tools are idempotent before enabling retries on tool-using agents.
- **Insufficient credits**: the SDK treats insufficient credits as a failover condition, but retrying the same provider will not resolve it, so Attempt never retries these failures. Provider failover remains the correct remedy.

Working with Results
--------------------

[](#working-with-results)

### The AttemptResult Object

[](#the-attemptresult-object)

When you call the `run` method instead of `thenReturn`, you receive an `AttemptResult` object that provides detailed information about the attempt:

```
$result = Attempt::try($callable)->run();

// Check status
$result->succeeded();  // bool
$result->failed();     // bool

// Get values
$result->value();      // mixed - the result value
$result->exception();  // ?Throwable - the exception if failed
$result->attempts();   // int - number of attempts made
$result->resolvedBy(); // string - 'primary', 'retry:2', 'fallback:ClassName'
```

### Monadic Operations

[](#monadic-operations)

The `AttemptResult` object supports monadic operations for functional-style programming:

```
$result->map(fn($value) => transform($value));
$result->getOrElse('default');
$result->getOrThrow();
$result->onSuccess(fn($value) => doSomething($value));
$result->onFailure(fn($e) => handleError($e));
```

Events
------

[](#events)

Attempt dispatches events throughout the attempt lifecycle, allowing you to hook into various stages for logging, monitoring, or other purposes:

EventWhen Fired`AttemptStarted`When the attempt begins`AttemptSucceeded`On successful completion`AttemptFailed`On each failure (before retry)`RetryAttempted`When a retry is initiated`FallbackTriggered`When a fallback is tried`AllAttemptsFailed`When all attempts and fallbacks failIf you need to disable events for a specific attempt, use the `withoutEvents` method:

```
Attempt::try($callable)
    ->withoutEvents()
    ->thenReturn();
```

Testing
-------

[](#testing)

Attempt includes a convenient fake implementation for testing. Use the `fake` method to replace the Attempt facade with a test double:

```
use Yannelli\Attempt\Facades\Attempt;

it('retries on failure', function () {
    Attempt::fake()->sequence([
        new ConnectionException('Failed'),
        new ConnectionException('Failed'),
        ['success' => true],
    ]);

    $result = Attempt::try(MyApiCall::class)
        ->retry(3)
        ->run();

    expect($result->succeeded())->toBeTrue();
    expect($result->attempts())->toBe(3);

    Attempt::assertAttemptedTimes(MyApiCall::class, 3);
});

it('uses fallback when all retries fail', function () {
    Attempt::fake()->failFor(PrimaryApi::class, times: 5);

    $result = Attempt::try(PrimaryApi::class)
        ->retry(3)
        ->fallback(BackupApi::class)
        ->run();

    Attempt::assertFallbackUsed(BackupApi::class);
});
```

To run the package’s test suite:

```
composer test
```

Configuration
-------------

[](#configuration)

The published configuration file (`config/attempt.php`) allows you to customize named backoff strategies, queue behavior, events, and global exception retry rules:

```
return [
    'backoff_strategies' => [
        'exponential' => [...],
        'linear' => [...],
        'fibonacci' => [...],
        'decorrelated_jitter' => [...],
    ],

    'async' => [
        'connection' => env('ATTEMPT_QUEUE_CONNECTION'),
        'queue' => env('ATTEMPT_QUEUE'),
        'timeout' => 60,
    ],

    'events' => [
        'enabled' => true,
    ],

    // Exceptions that should never trigger retries
    'never_retry' => [
        ValidationException::class,
        AuthenticationException::class,
        AuthorizationException::class,
        ModelNotFoundException::class,
    ],

    // Exceptions that should always trigger retries
    'always_retry' => [
        ConnectionException::class,
    ],
];
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Ryan Yannelli](https://ryanyannelli.com)
- [Nextvisit AI](https://nextvisit.ai)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

52

—

FairBetter than 96% of packages

Maintenance94

Actively maintained with recent releases

Popularity30

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 97.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~32 days

Total

7

Last Release

19d ago

Major Versions

v0.1.1 → v1.0.02026-03-14

PHP version history (2 changes)v0.1.0PHP ^8.2

v1.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/53b64331d4de8c9cef47bc20707ed728c0e900cd2bda4ed7d95359ced8b9adf1?d=identicon)[yannelli](/maintainers/yannelli)

---

Top Contributors

[![yannelli](https://avatars.githubusercontent.com/u/59575788?v=4)](https://github.com/yannelli "yannelli (36 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

backoffcomposercomposer-packageerror-handlingexception-handlingexponential-backofffallbackfault-tolerancelaravellaravel-packagephppipelineresilienceretryretry-strategylaravelretryerror handlingfallbackattemptyannelli

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/yannelli-attempt/health.svg)

```
[![Health](https://phpackages.com/badges/yannelli-attempt/health.svg)](https://phpackages.com/packages/yannelli-attempt)
```

###  Alternatives

[spatie/laravel-medialibrary

Associate files with Eloquent models

6.2k45.4M704](/packages/spatie-laravel-medialibrary)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M683](/packages/laravel-scout)[illuminate/queue

The Illuminate Queue package.

20433.0M1.8k](/packages/illuminate-queue)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M322](/packages/laravel-ai)[laravel/horizon

Dashboard and code-driven configuration for Laravel queues.

4.2k99.8M355](/packages/laravel-horizon)[spatie/laravel-health

Monitor the health of a Laravel application

88212.7M188](/packages/spatie-laravel-health)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
