Why Job Middleware Belongs in Your Toolkit
Laravel's job middleware API — introduced quietly but matured significantly — lets you wrap the handle() call of any job with reusable pipeline stages. Think of it as route middleware, but for your queue workers. The payoff: your job class stays focused on its single responsibility while cross-cutting concerns like idempotency, throttling, and failure policy live in composable, testable middleware.
Enforcing Idempotency with a Cache Lock
Double-dispatching is a real production hazard: retries, webhook replays, and race conditions can all cause a job to run twice. A dedicated middleware solves this cleanly.
<?php
namespace App\Queue\Middleware;
use Illuminate\Support\Facades\Cache;
final class WithIdempotencyKey
{
public function __construct(
private readonly string $key,
private readonly int $ttlSeconds = 3600,
) {}
public function handle(mixed $job, callable $next): void
{
$acquired = Cache::lock(
"idempotency:{$this->key}",
$this->ttlSeconds
)->get();
if (! $acquired) {
// Already processed — silently discard
$job->delete();
return;
}
$next($job);
}
}
Attach it from the job itself:
public function middleware(): array
{
return [
new WithIdempotencyKey("charge:{$this->payment->id}"),
];
}
Using Cache::lock() with an atomic backend (Redis, DynamoDB) guarantees only one worker proceeds even under concurrent retries.
Fine-Grained Rate Limiting with RateLimited
Laravel ships Illuminate\Queue\Middleware\RateLimited, but you often need per-tenant or per-resource limits rather than a global cap. Build on top of it:
<?php
namespace App\Queue\Middleware;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Support\Facades\RateLimiter;
final class ThrottlePerTenant
{
public function __construct(private readonly int $tenantId) {}
public function handle(mixed $job, callable $next): void
{
(new RateLimited("tenant-jobs:{$this->tenantId}"))
->handle($job, $next);
}
}
Register the limiter in a service provider:
RateLimiter::for('tenant-jobs', function (mixed $job) {
// $job is the queue payload wrapper, not your class
return Limit::perMinute(60);
});
When the limit is hit, Laravel automatically releases the job back onto the queue with a calculated backoff — no manual release() needed.
Graceful Failure: Skip on Precondition Failure
Sometimes the right answer is not to retry at all. If the model a job depends on was deleted between dispatch and execution, retrying is pointless noise in your failed jobs table.
<?php
namespace App\Queue\Middleware;
use App\Exceptions\PreconditionFailedException;
final class SkipOnPreconditionFailure
{
public function handle(mixed $job, callable $next): void
{
try {
$next($job);
} catch (PreconditionFailedException $e) {
// Log for observability, but don't retry
logger()->warning('Job skipped: precondition failed', [
'job' => get_class($job),
'reason' => $e->getMessage(),
]);
$job->delete();
}
}
}
Inside handle(), throw PreconditionFailedException when the required state is absent:
public function handle(OrderRepository $orders): void
{
$order = $orders->find($this->orderId)
?? throw new PreconditionFailedException("Order {$this->orderId} not found");
// proceed safely
}
Composing Middleware in Order
Middleware runs in the order returned from middleware(). Put idempotency first so you bail out before rate-limit counters are consumed:
public function middleware(): array
{
return [
new WithIdempotencyKey("invoice:{$this->invoice->uuid}"),
new ThrottlePerTenant($this->invoice->tenant_id),
new SkipOnPreconditionFailure(),
];
}
Key Takeaways
- Job middleware is a first-class pipeline — use it to keep
handle()free of cross-cutting logic. - Atomic cache locks are the correct primitive for idempotency; avoid boolean cache flags that can't survive race conditions.
RateLimitedmiddleware releases automatically — you get exponential backoff for free without manualrelease()calls.- Delete, don't fail when a precondition is permanently unmet; it keeps your failed-jobs table meaningful.
- Middleware order matters: idempotency → throttle → domain guards is the safest composition sequence.