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
phpuse 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:
phpuse 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:
phpuse 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
- 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.
- Stacking Skip and FailOnException in the wrong order — middleware runs in array order. Put
SkipbeforeFailOnException, so skipped jobs never reach the exception-handling layer.
Best Practices
- Use
FailOnExceptionfor domain errors,ThrottlesExceptionsfor transient ones — declines are permanent; rate-limit errors are temporary and deserve retries. - Pair
Skipwith feature flags — lets ops disable a job type without a deploy.
Summary
FailOnExceptionfails a job immediately on specific exception types.Skip::when/Skip::unlessdelete a job based on a closure.ThrottlesExceptionsWithRedisandRateLimitedWithRedisare Lua-based, race-free alternatives for Redis queues.