Skip to content

Laravel Job Middleware: Rate Limiting, Throttling, and Skipping Jobs Without Hacks

Job middleware in Laravel lets you wrap queue job execution with reusable logic. Learn how to build rate-limited, throttleable, and conditionally skipped jobs cleanly — no static flags or queue hacks required.

Published · Updated · 4 min read

Laravel Job Middleware: Rate Limiting, Throttling, and Skipping Jobs Without Hacks

Laravel Job Middleware: Rate Limiting, Throttling, and Skipping Jobs Without Hacks

Laravel's job middleware feature is one of the most underused tools in the queue ecosystem. Most teams reach for static flags, database locks, or custom queue drivers when they need to control job execution flow. Job middleware gives you a cleaner, composable alternative — and it ships with the framework.

What Job Middleware Actually Is

A job middleware is a plain PHP class with a single handle(object $job, Closure $next) method. You attach it via the middleware() method on your job class. Laravel calls each middleware in order before invoking handle() on the job itself.

class EnsureUserIsActive
{
    public function handle(object $job, Closure $next): void
    {
        if (! $job->user->is_active) {
            $job->delete();
            return;
        }

        $next($job);
    }
}

Attach it to a job:

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

No traits, no base classes — just composition.

Built-In Rate Limiting with RateLimited

Laravel ships Illuminate\Queue\Middleware\RateLimited, which integrates with the RateLimiter facade. Define a named limiter in AppServiceProvider (or a dedicated provider):

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;

RateLimiter::for('stripe-webhooks', function (object $job) {
    return Limit::perMinute(30)->by($job->accountId);
});

Then apply it:

use Illuminate\Queue\Middleware\RateLimited;

public function middleware(): array
{
    return [new RateLimited('stripe-webhooks')];
}

When the limit is exceeded, the job is automatically released back onto the queue with an exponential backoff. You can control the release delay:

new RateLimited('stripe-webhooks')->dontRelease()
// or
new RateLimited('stripe-webhooks')->releaseAfterBackoff($this->attempts())

releaseAfterBackoff uses the job's attempt count to compute a sensible delay — critical for avoiding thundering-herd re-queues after a rate limit window resets.

Throttling Bursts with WithoutOverlapping

WithoutOverlapping prevents concurrent execution of the same logical job:

use Illuminate\Queue\Middleware\WithoutOverlapping;

public function middleware(): array
{
    return [
        (new WithoutOverlapping($this->userId))
            ->releaseAfter(30)
            ->expireAfter(180),
    ];
}

The key insight: expireAfter sets a TTL on the atomic lock so a crashed worker doesn't block the job forever. Always set it. The default is no expiry — a silent footgun in production.

Building a Custom Skip Middleware

Sometimes you need to discard a job based on state that only exists at execution time — not dispatch time. A skip middleware handles this cleanly:

class SkipIfAlreadyProcessed
{
    public function handle(object $job, Closure $next): void
    {
        $key = 'processed:' . $job->idempotencyKey;

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

        $next($job);

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

This pattern is particularly useful for webhook processors and import jobs where the same payload can arrive multiple times.

Composing Multiple Middleware

Middleware compose naturally — order matters:

public function middleware(): array
{
    return [
        new SkipIfAlreadyProcessed,
        new RateLimited('external-api'),
        (new WithoutOverlapping($this->resourceId))->releaseAfter(10),
    ];
}

The skip check runs first, avoiding unnecessary lock acquisition and rate-limit counter increments for duplicate jobs.

Testing Job Middleware in Isolation

Because middleware are plain classes, they're trivially testable with Pest:

it('deletes the job when user is inactive', function () {
    $user = User::factory()->inactive()->make();
    $job = Mockery::mock(ProcessUserReport::class);
    $job->user = $user;
    $job->shouldReceive('delete')->once();

    $next = fn () => null;
    (new EnsureUserIsActive)->handle($job, $next);
});

No queue faking, no HTTP calls — pure unit test.

Takeaways

  • Job middleware is the correct abstraction for cross-cutting queue concerns — not base job classes or static state.
  • Always set expireAfter on WithoutOverlapping to prevent permanent lock starvation after worker crashes.
  • Use releaseAfterBackoff with RateLimited to avoid thundering-herd requeues at window reset.
  • Skip middleware should call $job->delete() explicitly — releasing without deleting re-queues the job.
  • Compose middleware in skip-first order to avoid wasting rate-limit budget on jobs that will be discarded anyway.
  • Plain classes mean plain unit tests — no queue infrastructure required.

Frequently asked questions

Does job middleware work with batched and chained jobs?

Yes. Middleware defined in a job's `middleware()` method applies regardless of whether the job is dispatched standalone, inside a batch, or as part of a chain. Each job in the batch or chain runs its own middleware independently.

What happens to a rate-limited job's attempt count when it is released back to the queue?

Releasing a job does not increment its attempt count. Only a failed execution increments `$this->attempts()`. This means a job can be released many times by rate-limiting middleware without burning through its `$tries` limit.

Can I pass constructor arguments to job middleware dynamically from the job?

Yes. Since middleware are instantiated inside the job's `middleware()` method, you have full access to `$this` and can pass any job property to the middleware constructor — for example, `new WithoutOverlapping($this->tenantId . ':' . $this->resourceId)`.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project