Laravel Queue Backpressure &amp; Dead-Letter Patterns | 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 at Scale: Backpressure, Dead-Letter Patterns, and Job Observability

 Laravel Queues at Scale: Backpressure, Dead-Letter Patterns, and Job Observability
===================================================================================

 Beyond basic queue workers: learn how to implement backpressure signals, dead-letter queues, and structured job observability in Laravel without reaching for a full message broker.

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

ShareCopy linkCopied

 ![Laravel Queues at Scale: Backpressure, Dead-Letter Patterns, and Job Observability](https://cdn.msaied.com/484/7ee0710bfc61c6ff7667dd4f60a8c5bd.png) 

  On this page +1. [The Problem Nobody Talks About](#the-problem-nobody-talks-about)
2. [Backpressure: Slowing the Producer](#backpressure-slowing-the-producer)
3. [Dead-Letter Queues Without a Broker](#dead-letter-queues-without-a-broker)
4. [Structured Job Observability](#structured-job-observability)
5. [Key Takeaways](#key-takeaways)

 The Problem Nobody Talks About
------------------------------

Most Laravel queue tutorials stop at `dispatch()` and `php artisan queue:work`. In production, the real challenges are: what happens when consumers fall behind producers, how do you safely handle poison-pill jobs, and how do you get meaningful signals out of your workers without drowning in noise?

This article addresses all three.

---

Backpressure: Slowing the Producer
----------------------------------

Backpressure means the consumer signals the producer to slow down when it cannot keep up. Laravel has no built-in backpressure primitive, but you can build one cheaply with Redis and a rate-aware dispatcher.

```php
// app/Queue/BackpressureDispatcher.php
final class BackpressureDispatcher
{
    private const THRESHOLD = 5_000;

    public function __construct(
        private readonly Redis $redis,
        private readonly string $queue = 'default',
    ) {}

    public function dispatch(ShouldQueue $job): void
    {
        $depth = (int) $this->redis->llen('queues:' . $this->queue);

        if ($depth >= self::THRESHOLD) {
            throw new QueueSaturatedException(
                "Queue [{$this->queue}] depth {$depth} exceeds threshold."
            );
        }

        dispatch($job)->onQueue($this->queue);
    }
}

```

The caller catches `QueueSaturatedException` and either retries with exponential back-off or returns a `202 Accepted` with a `Retry-After` header. This keeps your queue depth bounded without dropping work.

---

Dead-Letter Queues Without a Broker
-----------------------------------

Laravel's `failed_jobs` table is a dead-letter store, but it lacks routing. A better pattern is a dedicated `dead-letter` queue that jobs are explicitly moved to after exhausting retries, preserving the original payload and failure context.

```php
// app/Jobs/Concerns/RoutesToDeadLetter.php
trait RoutesToDeadLetter
{
    public function failed(Throwable $e): void
    {
        DeadLetterJob::dispatch(
            originalJob: static::class,
            payload: serialize($this),
            reason: $e->getMessage(),
            failedAt: now(),
        )->onQueue('dead-letter');
    }
}

```

```php
// app/Jobs/DeadLetterJob.php
final class DeadLetterJob implements ShouldQueue
{
    public int $tries = 1;

    public function __construct(
        public readonly string $originalJob,
        public readonly string $payload,
        public readonly string $reason,
        public readonly Carbon $failedAt,
    ) {}

    public function handle(): void
    {
        // Persist to dead_letter_events table for audit + replay UI
        DeadLetterEvent::create([
            'job' => $this->originalJob,
            'payload' => $this->payload,
            'reason' => $this->reason,
            'failed_at' => $this->failedAt,
        ]);
    }
}

```

A Filament resource over `dead_letter_events` gives ops teams a replay button without touching `artisan queue:retry`.

---

Structured Job Observability
----------------------------

Log lines are noise. Structured spans are signal. Use Laravel's `Queue::before` and `Queue::after` hooks to emit consistent telemetry.

```php
// app/Providers/QueueServiceProvider.php
public function boot(): void
{
    Queue::before(function (JobProcessing $event) {
        $this->startTimer($event->job->getJobId());
    });

    Queue::after(function (JobProcessed $event) {
        $elapsed = $this->stopTimer($event->job->getJobId());

        Log::channel('structured')->info('job.processed', [
            'job' => $event->job->resolveName(),
            'queue' => $event->job->getQueue(),
            'duration_ms' => $elapsed,
            'attempts' => $event->job->attempts(),
            'connection' => $event->connectionName,
        ]);
    });

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

```

Pipe the `structured` channel to a JSON log driver and ingest into your observability stack (Grafana Loki, Datadog, etc.). You now have p95 job latency per class without a paid APM agent.

---

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

- **Backpressure** is a producer responsibility — check queue depth before dispatching and surface `Retry-After` to callers.
- **Dead-letter queues** should be first-class entities with a replay UI, not a graveyard in `failed_jobs`.
- **Structured telemetry** from `Queue::before/after` hooks gives you latency histograms and failure rates per job class for free.
- Keep `$tries` low (2–3) and `$backoff` exponential; long retry chains mask systemic failures.
- A dedicated `dead-letter` queue worker with a single process prevents replay storms from starving production queues.

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

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

  Should I use a real message broker like RabbitMQ instead of building these patterns in Laravel?For most SaaS workloads, Redis-backed Laravel queues with explicit backpressure and dead-letter routing are sufficient and far simpler to operate. Reach for a broker when you need multi-consumer fan-out, cross-service routing, or guaranteed ordering across partitions — not just because your queue is busy.

   How do I replay jobs from the dead-letter table without re-queuing poison pills?Deserialize the stored payload, inspect it, optionally patch it, then dispatch a fresh instance. Add a `replayed\_from` metadata field so you can detect replay loops and halt after a configurable number of replay attempts per original job ID.

   Does checking queue depth on every dispatch add meaningful latency?An Redis LLEN call is a single O(1) command, typically sub-millisecond on a local or same-datacenter Redis. Cache the result for 500ms with an in-memory static property if you dispatch in tight loops.

   ![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 Typed Enums as First-Class Citizens: Casts, Rules, and PHP 8.3 Features](https://www.msaied.com/public/articles/laravel-typed-enums-as-first-class-citizens-casts-rules-and-php-83-features) [Next articleEverything Announced at Laracon US 2026: Laravel Framework, Cloud &amp; AI Updates](https://www.msaied.com/public/articles/everything-announced-at-laracon-us-2026-laravel-framework-cloud-ai-updates)  

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

1. [The Problem Nobody Talks About](#the-problem-nobody-talks-about)
2. [Backpressure: Slowing the Producer](#backpressure-slowing-the-producer)
3. [Dead-Letter Queues Without a Broker](#dead-letter-queues-without-a-broker)
4. [Structured Job Observability](#structured-job-observability)
5. [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)
