Laravel Queue Retries, Dead-Letter &amp; Observability | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Queues: Reliable Retry Strategies, Dead-Letter Handling, and Observability

 Laravel Queues: Reliable Retry Strategies, Dead-Letter Handling, and Observability
===================================================================================

 Beyond basic queue configuration: learn how to design retry policies, implement dead-letter queues, and add structured observability to Laravel queue workers so failures never silently disappear in production.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 2 Jul 2026 · Updated 2 Jul 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Queues: Reliable Retry Strategies, Dead-Letter Handling, and Observability](https://cdn.msaied.com/342/61eb6bda971f2b42e9a96f6b8fd0a302.png) 

  On this page +1. [The Problem With Default Queue Configuration](#the-problem-with-default-queue-configuration)
2. [Retry Policies That Actually Match Failure Modes](#retry-policies-that-actually-match-failure-modes)
3. [Implementing a Dead-Letter Queue](#implementing-a-dead-letter-queue)
4. [Structured Observability Without a Full APM](#structured-observability-without-a-full-apm)
5. [Tracking Duration Properly](#tracking-duration-properly)
6. [Key Takeaways](#key-takeaways)

 The Problem With Default Queue Configuration
--------------------------------------------

Laravel ships with sensible queue defaults, but "sensible" rarely means "production-ready". The default `tries = 1`, no backoff, and a single `failed_jobs` table give you a place to store failures — not a strategy for recovering from them. This article focuses on three concrete improvements: structured retry policies, a proper dead-letter pattern, and lightweight observability without a full APM stack.

---

Retry Policies That Actually Match Failure Modes
------------------------------------------------

Not all failures are equal. A transient HTTP 429 from a third-party API needs exponential backoff. A validation failure should never retry at all. Encode that distinction directly in the job.

```php
final class SyncExternalOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public int $maxExceptions = 3;

    public function backoff(): array
    {
        // Exponential: 10s, 60s, 300s, 600s
        return [10, 60, 300, 600];
    }

    public function handle(OrderSyncService $service): void
    {
        try {
            $service->sync($this->orderId);
        } catch (ValidationException $e) {
            // Permanent failure — do not retry
            $this->fail($e);
        }
    }
}

```

`$maxExceptions` caps retries on uncaught exceptions independently of `$tries`, which is useful when you want to allow manual releases (`$this->release()`) without burning retry budget on expected waits.

---

Implementing a Dead-Letter Queue
--------------------------------

Laravel's `failed_jobs` table is a graveyard, not a queue. A dead-letter queue (DLQ) is a real queue you can inspect, replay, and alert on.

```php
// In your job's failed() method
public function failed(Throwable $e): void
{
    DeadLetterJob::dispatch([
        'original_job' => static::class,
        'payload' => $this->toArray(),
        'exception' => $e->getMessage(),
        'failed_at' => now()->toIso8601String(),
    ])->onQueue('dead-letter');

    Log::channel('slack')->critical('Job permanently failed', [
        'job' => static::class,
        'order_id' => $this->orderId,
        'error' => $e->getMessage(),
    ]);
}

```

`DeadLetterJob` is a simple passthrough that stores the serialized payload in a dedicated table or Redis stream. You can replay it with a custom Artisan command:

```php
protected function handle(): void
{
    DeadLetterEntry::unresolved()->each(function (DeadLetterEntry $entry) {
        $jobClass = $entry->original_job;
        $jobClass::dispatch(...$entry->reconstructedArgs())
            ->onQueue('default');
        $entry->markReplayed();
    });
}

```

This keeps your `failed_jobs` table for diagnostics and your DLQ for operational recovery — two different concerns.

---

Structured Observability Without a Full APM
-------------------------------------------

You don't need Datadog to know what your queues are doing. Laravel's queue events give you everything you need to emit structured logs.

```php
// AppServiceProvider::boot()
Queue::before(function (JobProcessing $event) {
    Log::info('job.started', [
        'job' => $event->job->resolveName(),
        'queue' => $event->job->getQueue(),
        'attempt' => $event->job->attempts(),
    ]);
});

Queue::after(function (JobProcessed $event) {
    Log::info('job.completed', [
        'job' => $event->job->resolveName(),
        'duration_ms' => /* track via context */ null,
    ]);
});

Queue::failing(function (JobFailed $event) {
    Log::error('job.failed', [
        'job' => $event->job->resolveName(),
        'exception' => $event->exception->getMessage(),
        'attempt' => $event->job->attempts(),
    ]);
});

```

Pair this with a log aggregator (Loki, Papertrail, CloudWatch) and you get queue throughput, failure rates, and attempt distributions without any additional dependencies.

### Tracking Duration Properly

Use a request-scoped context value to measure wall time:

```php
Queue::before(fn () => app()->instance('job.start', microtime(true)));

Queue::after(function (JobProcessed $event) {
    $duration = (microtime(true) - app('job.start')) * 1000;
    Log::info('job.completed', [
        'job' => $event->job->resolveName(),
        'duration_ms' => round($duration, 2),
    ]);
});

```

Because Octane workers are long-lived, always rebind `job.start` in `before` — never rely on a static property.

---

Key Takeaways
-------------

- **Match retry policy to failure mode**: use `backoff()` arrays for exponential delays, `$maxExceptions` for exception caps, and `$this->fail()` for permanent failures.
- **Separate diagnostics from recovery**: `failed_jobs` is for humans; a DLQ is for automated replay pipelines.
- **Queue events are free observability**: `Queue::before/after/failing` emit structured logs with zero overhead.
- **Avoid static state in workers**: always rebind per-job context in `Queue::before` when running under Octane or Swoole.
- **Test failure paths explicitly**: assert that `failed()` dispatches to your DLQ in Pest using `Queue::fake()`.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [queues](https://www.msaied.com/public/articles?search=queues)
- [reliability](https://www.msaied.com/public/articles?search=reliability)
- [observability](https://www.msaied.com/public/articles?search=observability)
- [php](https://www.msaied.com/public/articles?search=php)

 Frequently asked questions 
---------------------------

  What is the difference between `$tries` and `$maxExceptions` in a Laravel job?`$tries` is the total number of times a job may be attempted, including manual `release()` calls. `$maxExceptions` limits how many times an \*unhandled exception\* can occur before the job is permanently failed, independent of the attempt count. Use both together when you want to allow deliberate delays via `release()` without burning your exception budget.

   Should I use the `failed\_jobs` table or a dedicated dead-letter queue?Use both for different purposes. `failed\_jobs` is a diagnostic record — it tells you what failed and why. A dead-letter queue is an operational tool: it holds serializable payloads you can replay, filter, and alert on programmatically. The `failed\_jobs` table has no built-in replay mechanism beyond `queue:retry`, which doesn't scale for complex recovery workflows.

   Are Queue event listeners safe to use under Laravel Octane?Yes, but be careful with any state you bind into the container inside those listeners. Because Octane workers are long-lived, anything bound in `Queue::before` must be rebound on every job — never assume the previous job's context has been cleared. Binding a fresh value in `before` and reading it in `after` is safe as long as you don't use static properties or module-level variables.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel June 2026 Product Updates: Bus::bulk(), Symfony on Cloud, Forge Valkey &amp; More](https://www.msaied.com/public/articles/laravel-june-2026-product-updates-busbulk-symfony-on-cloud-forge-valkey-more) [Next articlephpcpd-next: A Modern Copy/Paste Detector CLI for PHP 8.5+](https://www.msaied.com/public/articles/phpcpd-next-a-modern-copypaste-detector-cli-for-php-85)  

   On this page
-------------

1. [The Problem With Default Queue Configuration](#the-problem-with-default-queue-configuration)
2. [Retry Policies That Actually Match Failure Modes](#retry-policies-that-actually-match-failure-modes)
3. [Implementing a Dead-Letter Queue](#implementing-a-dead-letter-queue)
4. [Structured Observability Without a Full APM](#structured-observability-without-a-full-apm)
5. [Tracking Duration Properly](#tracking-duration-properly)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
