Laravel Queue Job Middleware &amp; Idempotency | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel Queues: Reliable Job Middleware, Idempotency, and Graceful Failure Handling        On this page       1. [  The Problem With Naive Job Dispatch ](#the-problem-with-naive-job-dispatch)
2. [  Custom Job Middleware ](#custom-job-middleware)
3. [  Idempotency at the Database Level ](#idempotency-at-the-database-level)
4. [  Distinguishing Transient vs. Permanent Failures ](#distinguishing-transient-vs-permanent-failures)
5. [  Combining Everything With a Base Job ](#combining-everything-with-a-base-job)
6. [  Key Takeaways ](#key-takeaways)

  ![Laravel Queues: Reliable Job Middleware, Idempotency, and Graceful Failure Handling](https://cdn.msaied.com/280/3b6ba5c26ef22b77f3c39c5d9a6b76e9.png)

  #laravel   #queues   #jobs   #reliability  

 Laravel Queues: Reliable Job Middleware, Idempotency, and Graceful Failure Handling 
=====================================================================================

     24 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Naive Job Dispatch  ](#the-problem-with-naive-job-dispatch)
2. [  02   Custom Job Middleware  ](#custom-job-middleware)
3. [  03   Idempotency at the Database Level  ](#idempotency-at-the-database-level)
4. [  04   Distinguishing Transient vs. Permanent Failures  ](#distinguishing-transient-vs-permanent-failures)
5. [  05   Combining Everything With a Base Job  ](#combining-everything-with-a-base-job)
6. [  06   Key Takeaways  ](#key-takeaways)

 The Problem With Naive Job Dispatch
-----------------------------------

Most Laravel queue tutorials stop at `dispatch(new SendInvoice($order))`. That works fine until a worker crashes mid-execution, a downstream API times out, or a deployment restarts Horizon at exactly the wrong moment. The job retries, the invoice is sent twice, and your customer is furious.

Production queues demand three things: **custom job middleware** for cross-cutting concerns, **idempotency guards** to prevent duplicate side-effects, and **structured failure handling** that distinguishes transient errors from permanent ones.

---

Custom Job Middleware
---------------------

Job middleware in Laravel is underused. Unlike HTTP middleware, it wraps the `handle()` call directly and can short-circuit execution cleanly.

```php
// app/Queue/Middleware/SkipIfAlreadyProcessed.php
class SkipIfAlreadyProcessed
{
    public function handle(object $job, callable $next): void
    {
        $key = 'job_processed:' . $job->idempotencyKey();

        if (Cache::has($key)) {
            $job->delete();
            return;
        }

        $next($job);

        Cache::put($key, true, now()->addHours(24));
    }
}

```

Attach it in the job itself:

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

    public function __construct(public readonly Order $order) {}

    public function middleware(): array
    {
        return [new SkipIfAlreadyProcessed];
    }

    public function idempotencyKey(): string
    {
        return 'invoice:' . $this->order->id;
    }

    public function handle(InvoiceService $service): void
    {
        $service->send($this->order);
    }
}

```

This pattern keeps idempotency logic out of `handle()` and makes it reusable across job classes.

---

Idempotency at the Database Level
---------------------------------

Cache-based guards are fast but not durable across cache flushes. For financial or notification jobs, back the idempotency key with a database record.

```php
Schema::create('processed_jobs', function (Blueprint $table) {
    $table->string('idempotency_key')->primary();
    $table->timestamp('processed_at');
});

```

```php
class DatabaseIdempotency
{
    public function handle(object $job, callable $next): void
    {
        $key = $job->idempotencyKey();

        $inserted = DB::table('processed_jobs')->insertOrIgnore([
            'idempotency_key' => $key,
            'processed_at'    => now(),
        ]);

        if ($inserted === 0) {
            $job->delete();
            return;
        }

        try {
            $next($job);
        } catch (Throwable $e) {
            DB::table('processed_jobs')->where('idempotency_key', $key)->delete();
            throw $e;
        }
    }
}

```

`insertOrIgnore` is atomic on a primary key conflict. If the job throws, we delete the record so a retry can proceed — but only for transient failures.

---

Distinguishing Transient vs. Permanent Failures
-----------------------------------------------

Not every exception should trigger a retry. A `PaymentGatewayUnavailableException` is transient; a `CardPermanentlyDeclinedException` is not.

```php
public function failed(Throwable $exception): void
{
    if ($exception instanceof PermanentFailureException) {
        // Notify, log, and do NOT re-queue
        Log::error('Permanent job failure', [
            'job'   => static::class,
            'order' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);
        return;
    }

    // For transient failures, let Laravel's retry logic handle it
    // but cap attempts to avoid infinite loops
}

public int $tries = 5;
public int $backoff = 60; // seconds between retries

```

Pair this with `$this->fail($exception)` inside `handle()` when you detect a permanent condition — it marks the job failed immediately without exhausting retry attempts.

---

Combining Everything With a Base Job
------------------------------------

Avoid repeating middleware declarations across dozens of job classes:

```php
abstract class ReliableJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries   = 5;
    public int $backoff = 30;

    abstract public function idempotencyKey(): string;

    public function middleware(): array
    {
        return [new DatabaseIdempotency];
    }
}

class SendInvoice extends ReliableJob
{
    public function __construct(public readonly Order $order) {}

    public function idempotencyKey(): string
    {
        return 'invoice:' . $this->order->id;
    }

    public function handle(InvoiceService $service): void
    {
        $service->send($this->order);
    }
}

```

Every subclass inherits idempotency and retry configuration automatically.

---

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

- **Job middleware** is the right place for cross-cutting concerns like idempotency, rate-limiting, and telemetry — keep `handle()` focused on domain logic.
- **`insertOrIgnore` on a primary key** gives you atomic, durable deduplication without race conditions.
- **Roll back the idempotency record on transient failure** so retries can proceed; leave it in place on permanent failure.
- **`$this->fail()`** short-circuits retry logic immediately — use it when you know a retry cannot succeed.
- A **shared base job class** enforces reliability conventions across your entire application without repetition.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-queues-reliable-job-middleware-idempotency-and-graceful-failure-handling&text=Laravel+Queues%3A+Reliable+Job+Middleware%2C+Idempotency%2C+and+Graceful+Failure+Handling) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-queues-reliable-job-middleware-idempotency-and-graceful-failure-handling) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Why use a database table for idempotency instead of Redis/Cache?        Cache entries can be evicted under memory pressure or lost during a Redis restart. A database primary key constraint is durable and atomic — `insertOrIgnore` guarantees exactly-once insertion even under concurrent workers processing the same job. 

      Q02  Does rolling back the idempotency record on failure cause duplicate processing?        Only intentionally. You delete the record only when the job throws, allowing a retry. For permanent failures you leave the record in place (or call `$this-&gt;fail()`) so the job is never retried. The key is classifying your exceptions correctly. 

      Q03  Can I use Laravel's built-in WithoutOverlapping middleware instead?        `WithoutOverlapping` prevents concurrent execution of the same job but does not prevent re-execution after a successful run. It solves a different problem — use it alongside idempotency guards, not instead of them. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Typed Enums as First-Class Domain Citizens in Laravel with PHP 8.3](https://cdn.msaied.com/282/71a8fc3e4cf4239b1bf6d38d57e0b985.png) laravel php8.3 enums 

### Typed Enums as First-Class Domain Citizens in Laravel with PHP 8.3

Go beyond simple enum labels. Learn how to attach behaviour, implement interfaces, and use backed enums as Elo...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/typed-enums-as-first-class-domain-citizens-in-laravel-with-php-83) [ ![RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation in Practice](https://cdn.msaied.com/281/8d2ac57c0e69d3ff9f1e68faf0e4d10c.png) laravel ai pgvector 

### RAG in Laravel: pgvector, Embeddings, and Retrieval-Augmented Generation in Practice

Build a production-ready RAG pipeline in Laravel using pgvector, OpenAI embeddings, and a clean retrieval laye...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/rag-in-laravel-pgvector-embeddings-and-retrieval-augmented-generation-in-practice) [ ![Ship AI with Laravel: Failover, Queues, and Middleware for AI Agents](https://cdn.msaied.com/283/f0a6d6a6f22d9131bacb96bae1bfc10b.png) Laravel AI Agents Queues 

### Ship AI with Laravel: Failover, Queues, and Middleware for AI Agents

Learn how to make Laravel AI agents production-ready with automatic provider failover, background queue proces...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 24 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/ship-ai-with-laravel-failover-queues-and-middleware-for-ai-agents) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
