Laravel job has been attempted too many times or run too long. The job may have previously timed out

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Decoding the Laravel Queue Nightmare: Why Jobs Fail with "Attempted Too Many Times"

As a senior developer working with asynchronous processing in Laravel, we frequently encounter frustrating errors like, "Job has been attempted too many times or run too long. The job may have previously timed out." This error, often surfaced through tools like Horizon, signals that the queue system has hit its configured limits for a specific job delivery.

When dealing with complex, stateful jobs—especially those involving external services like payment tracking where you need custom retry logic—the default Laravel retry mechanism isn't always sufficient. Let’s dissect your scenario and uncover what you might be missing in your configuration or job handling strategy.

Understanding the Timeout vs. Retry Mechanism

The error message can stem from two main causes: a timeout (the job took longer than the worker/queue allowed) or exceeding the maximum retry attempts. In your case, since your processing time is extremely fast (< 0.5s), a simple execution timeout is unlikely to be the primary culprit. This strongly suggests that the issue lies within how your custom logic interacts with the queue system's failure tracking mechanism, or in the global settings defined in config/queue.php.

Your current approach attempts to handle failures internally by calling $this->reQueueJob() and delaying the subsequent attempt. While this is a valid pattern for back-off strategies, it can inadvertently confuse the queue driver's internal attempt counting if not managed carefully alongside Laravel’s built-in retry system.

Deconstructing Queue Configuration Limits

You mentioned checking config/queue.php and config/horizon.php, but often the specific limits that control retries are defined at a lower level or implicitly by the driver itself (like Redis).

In your Redis configuration:

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 960, // 16 minutes
    'block_for' => null,
],

The retry_after setting controls how long the queue system waits before re-queueing a failed job. While increasing this value helps with waiting periods, it doesn't inherently reset or limit the total number of jobs that can be retried indefinitely. If your custom logic repeatedly re-dispatches the job without allowing for a final failure state, the system eventually flags it as an excessive attempt.

Implementing Robust Retry Strategies

Since you are implementing complex, time-gated retry logic (waiting 15 minutes before updating status), relying solely on the queue's built-in attempts might be insufficient. A more robust pattern involves separating transient failures from permanent failures.

1. Leverage Laravel’s Built-in Retries

For standard errors, let Laravel manage the retries. When defining your job, use the tries and backoff properties:

class TrackOrderPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Define the maximum number of times the job should be attempted.
     */
    public int $tries = 5; // Set a reasonable limit

    /**
     * Define the delay in seconds between retries (exponential backoff is recommended).
     */
    public function backoff(): array
    {
        // Exponential backoff: 60s, 120s, 240s, 480s...
        return [60, 120, 240, 480];
    }

    public function handle()
    {
        try {
            // ... your payment tracking logic
        } catch (Throwable $exception) {
            // If we fail here, let the framework handle the standard retry mechanism.
            // Custom re-queuing should be reserved for specific business rules, not general failures.
            throw $exception; 
        }
    }
}

2. The State Management Approach (The Better Way)

Instead of relying on the queue to manage complex time-based state, treat the job execution as an event that updates a persistent record. If the payment isn't detected after 15 minutes, the cron job should handle the status update, and the failed job should be considered complete or moved to a Dead Letter Queue (DLQ).

If you must implement custom delays within the job itself, ensure your state management is explicit. For complex asynchronous flows, leveraging services that manage workflow states, as discussed in modern Laravel architecture, provides far more resilience than manipulating queue retry counts directly. This focus on clean architecture is central to building scalable applications, much like the principles outlined by organizations focusing on robust development practices found at sites like Laravel Company.

Conclusion

The error "attempted too many times or run too long" is a symptom of hitting system constraints, not necessarily a flaw in your processing speed. By moving away from custom, indefinite re-queuing within the handle() method and instead adopting Laravel's structured retry mechanisms combined with external state tracking (like your cron job), you create a more predictable and resilient asynchronous workflow. Focus on what the job should do, and let the queue system manage the delivery lifecycle efficiently.