What is Job Middleware?
HTTP middleware wraps around incoming web requests. Job middleware does the same thing for queued jobs —
it wraps around the handle() method and lets you add cross-cutting logic
without cluttering the job class itself.
Without job middleware, you end up with handle() methods full of boilerplate:
rate limit checks, overlap checks, retry logic, feature flag checks.
Job middleware moves all that out of the job and into a reusable, testable class.
Job middleware can be applied to any queueable class:
Creating Custom Job Middleware
There's no Artisan generator for job middleware — you create the class manually.
By convention, place it in app/Jobs/Middleware/.
A job middleware class needs a single handle() method that receives the job and a $next closure:
<?php
namespace App\Jobs\Middleware;
use Closure;
class EnsureFeatureIsEnabled
{
public function __construct(protected string $feature) {}
public function handle(object $job, Closure $next): void
{
// Check a feature flag before running the job
if (! feature_enabled($this->feature)) {
// Silently delete the job — feature is off
$job->delete();
return;
}
// Feature is enabled, proceed with the job
$next($job);
}
}
A Practical Example: Logging Middleware
Here's a middleware that logs job start and finish times — useful for identifying slow jobs in production:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class LogJobExecution
{
public function handle(object $job, Closure $next): void
{
$jobName = class_basename($job);
$start = microtime(true);
Log::info("[Queue] Starting: {$jobName}");
$next($job);
$duration = round(microtime(true) - $start, 2);
Log::info("[Queue] Finished: {$jobName} in {$duration}s");
}
}
Attaching Middleware to Jobs
Add a middleware() method to your job class that returns an array of middleware instances:
use App\Jobs\Middleware\EnsureFeatureIsEnabled;
use App\Jobs\Middleware\LogJobExecution;
class SendWeeklyDigest implements ShouldQueue
{
public function middleware(): array
{
return [
new EnsureFeatureIsEnabled('weekly-digest'),
new LogJobExecution(),
];
}
public function handle(): void
{
// send the weekly digest email
}
}
Built-in: RateLimited
The RateLimited middleware throttles how many jobs of a type run within a time period.
This is essential when calling external APIs that have rate limits (e.g. 100 requests/minute).
Define the Rate Limiter
First, define a named rate limiter in AppServiceProvider::boot():
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
// In App\Providers\AppServiceProvider::boot()
RateLimiter::for('external-crm', function (object $job) {
return Limit::perMinute(30); // max 30 jobs per minute
});
// Per-user rate limit
RateLimiter::for('notifications', function (object $job) {
return Limit::perMinute(5)->by($job->user->id); // 5 per user per minute
});
Apply the Middleware
use Illuminate\Queue\Middleware\RateLimited;
class SyncToCrm implements ShouldQueue
{
public int $tries = 10; // allow retries while rate limit is active
public array $backoff = [10, 30, 60];
public function middleware(): array
{
return [new RateLimited('external-crm')];
}
public function handle(): void
{
// call the CRM API
}
}
When the rate limit is hit, the job is automatically released back to the queue
and retried after the rate limit window resets.
RateLimited with Redis Throttle (Custom)
For more control, you can build a custom rate-limited middleware using Redis directly —
useful when you need token-bucket rate limiting instead of fixed windows:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RedisRateLimited
{
public function handle(object $job, Closure $next): void
{
Redis::throttle('stripe-api')
->block(0) // don't wait for lock, fail immediately
->allow(5) // allow 5 jobs
->every(10) // per 10 seconds
->then(function () use ($job, $next) {
// Lock obtained — run the job
$next($job);
}, function () use ($job) {
// Could not obtain lock — release back to queue in 10 seconds
$job->release(10);
});
}
}
⚠️ This Redis throttle middleware requires the Redis queue driver or at least Redis available in your app. It won't work correctly with the database queue driver alone.
Built-in: WithoutOverlapping
As covered in the Unique Jobs post, WithoutOverlapping prevents two instances
of the same job from running at the same time. Here's the complete usage:
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProcessUserReport implements ShouldQueue
{
public function __construct(protected int $userId) {}
public function middleware(): array
{
return [
(new WithoutOverlapping($this->userId))
->releaseAfter(30) // release back to queue after 30s if overlapping
->expireAfter(300) // the lock expires after 5 minutes (in case of crash)
];
}
public function handle(): void
{
// generate report for $this->userId
}
}
dontRelease — Skip Instead of Retry
// If another instance is running, just delete this job (don't retry)
return [(new WithoutOverlapping($this->userId))->dontRelease()];
The expireAfter() is important for production safety.
If a worker crashes mid-job, the lock would normally stay forever (preventing future runs).
Setting an expiry ensures the lock eventually releases even if the job never finished cleanly.
Built-in: ThrottleExceptions
ThrottleExceptions is for situations where your job calls an external service
that's temporarily down. Instead of burning through all your retries immediately,
this middleware slows down retries when exceptions start occurring.
use Illuminate\Queue\Middleware\ThrottleExceptions;
class CallPaymentGateway implements ShouldQueue
{
public int $tries = 10;
public function middleware(): array
{
return [
// Allow 3 exceptions per 5 minutes
// If exceeded, release the job and wait 5 minutes before trying again
new ThrottleExceptions(3, 5),
];
}
public function handle(): void
{
// call payment API
}
}
Practical use case: your payment gateway goes down. Without this middleware, your worker
hammers the gateway 10 times in seconds, fails all retries, and marks the job as permanently failed.
With ThrottleExceptions(3, 5), after 3 failures it backs off for 5 minutes —
giving the gateway time to recover.
ThrottleExceptions with byJob()
// Throttle per unique job instance (based on job ID), not globally
new ThrottleExceptions(3, 5)->byJob()
Built-in: Skip
The Skip middleware conditionally skips (silently deletes) a job based on a condition.
Useful for feature flags or dynamic conditions evaluated at dispatch time:
use Illuminate\Queue\Middleware\Skip;
class SendMarketingEmail implements ShouldQueue
{
public function __construct(protected User $user) {}
public function middleware(): array
{
return [
// Skip this job entirely if the user unsubscribed
Skip::when(fn () => ! $this->user->marketing_emails_enabled),
// Or: skip unless a condition is true
Skip::unless(fn () => $this->user->hasActiveSubscription()),
];
}
public function handle(): void
{
// send marketing email
}
}
Reusable Middleware Across Job Types
The biggest win of job middleware is reusability. Write the logic once, apply it everywhere.
Here's a real-world example — a middleware that skips jobs when the app is in maintenance mode:
<?php
namespace App\Jobs\Middleware;
use Closure;
class SkipDuringMaintenance
{
public function handle(object $job, Closure $next): void
{
if (app()->isDownForMaintenance()) {
// Release back to queue — retry in 60 seconds when maintenance ends
$job->release(60);
return;
}
$next($job);
}
}
Now apply it to any job that should pause during maintenance:
// Works on jobs, mailables, notifications, event listeners
class SendInvoice implements ShouldQueue
{
public function middleware(): array
{
return [new SkipDuringMaintenance()];
}
}
class ProcessOrder implements ShouldQueue
{
public function middleware(): array
{
return [new SkipDuringMaintenance()];
}
}
Conclusion
Job middleware transforms how you build queue systems. Instead of writing the same checks in
every job's handle() method, you centralize cross-cutting concerns in small, testable classes.
Create middleware in app/Jobs/Middleware/ with a handle(object $job, Closure $next) method
Return them from the job's middleware() method
Use RateLimited for API rate limiting — jobs wait instead of failing
Use WithoutOverlapping to prevent concurrent execution of the same job
Use ThrottleExceptions to back off when an external service is struggling
Use Skip for clean conditional job skipping
Write your own middleware for feature flags, maintenance mode, logging, and more