New Job Middleware in Laravel 13

+15 Mana ✨

Introduction

Laravel 13 ships four new queue middleware classes that tighten control over retry and rate-limit logic: FailOnException, Skip, ThrottlesExceptionsWithRedis, and RateLimitedWithRedis. They plug into the same middleware() method you already use for WithoutOverlapping and ThrottlesExceptions, so adoption is one import plus one array entry.

Key Concepts

  • FailOnException: "If this exception type is thrown, don't retry — fail immediately."
  • Skip: "If this condition is true, silently delete the job from the queue."
  • ThrottlesExceptionsWithRedis / RateLimitedWithRedis: Redis-optimized variants of the throttling middleware that use atomic Lua scripts instead of cache-based locks.

Real World Context

The classic pain point: a payment job retries 3 times after CardDeclinedException, burning queue slots even though a decline is never going to recover. Before Laravel 13, you had to catch and manually call $this->fail($e). Now FailOnException::class does it declaratively.

Deep Dive

FailOnException

php
use Illuminate\Queue\Middleware\FailOnException;
use App\Exceptions\CardDeclinedException;
use App\Exceptions\InvalidAddressException;

class ProcessPayment implements ShouldQueue
{
    public function middleware(): array
    {
        return [
            new FailOnException([
                CardDeclinedException::class,
                InvalidAddressException::class,
            ]),
        ];
    }
}

If the job throws either exception, it goes straight to failed_jobs — no retries, no backoff.

Skip

Skip deletes a job before it runs based on a closure. Useful when the dispatch and the handle may happen minutes apart and conditions might have changed:

php
use Illuminate\Queue\Middleware\Skip;

public function middleware(): array
{
    return [
        Skip::when(fn () => $this->order->cancelled),
        Skip::unless(fn () => feature('send-receipts')->active()),
    ];
}

Skip::when deletes if the closure returns true. Skip::unless deletes unless the closure returns true.

Redis-optimized throttling

If your queue runs on Redis, swap the generic middleware for the Redis-native versions:

php
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
use Illuminate\Queue\Middleware\RateLimitedWithRedis;

public function middleware(): array
{
    return [
        new ThrottlesExceptionsWithRedis(10, 5),  // 10 exceptions per 5 minutes
        new RateLimitedWithRedis('api-calls'),
    ];
}

Both use atomic Lua scripts for the counter, so they don't suffer the race conditions a cache-based lock can under heavy load.

Common Pitfalls

  1. Using the non-Redis throttles on a Redis queue — they work, but you leave performance on the table and may see duplicate executions under burst load.
  2. Stacking Skip and FailOnException in the wrong order — middleware runs in array order. Put Skip before FailOnException, so skipped jobs never reach the exception-handling layer.

Best Practices

  1. Use FailOnException for domain errors, ThrottlesExceptions for transient ones — declines are permanent; rate-limit errors are temporary and deserve retries.
  2. Pair Skip with feature flags — lets ops disable a job type without a deploy.

Summary

  • FailOnException fails a job immediately on specific exception types.
  • Skip::when / Skip::unless delete a job based on a closure.
  • ThrottlesExceptionsWithRedis and RateLimitedWithRedis are Lua-based, race-free alternatives for Redis queues.
✓ Completed