Laravel Rate-Limited Job Middleware Deep Dive | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com) - [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [About](https://www.msaied.com#about)

           [  Contact](https://www.msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com)
- [ProjectsCase studies](https://www.msaied.com/projects)
- [ArticlesEngineering notes](https://www.msaied.com/articles)
- [CertificatesCredentials](https://www.msaied.com/certificates)
- [AboutHow I work](https://www.msaied.com#about)
- [ContactGet in touch](https://www.msaied.com#contact)

  [Start a conversation](https://www.msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com)
2. /
3. [Articles](https://www.msaied.com/articles)
4. /
5. Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

 Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work
===========================================================================

 Rate-limited job middleware in Laravel lets you throttle expensive third-party calls at the worker level, not the HTTP layer. Learn how to build composable, Redis-backed throttles that survive retries and respect external API limits.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com#person) Published 4 Oct 2026 · Updated 4 Oct 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png) 

  On this page +1. [The Problem: Throttling at the Right Layer](#the-problem-throttling-at-the-right-layer)
2. [How RateLimited Works Under the Hood](#how-coderatelimitedcode-works-under-the-hood)
3. [Controlling Release Delay and Max Attempts](#controlling-release-delay-and-max-attempts)
4. [Composing Multiple Middleware](#composing-multiple-middleware)
5. [Testing Rate-Limited Jobs](#testing-rate-limited-jobs)
6. [Takeaways](#takeaways)

 The Problem: Throttling at the Right Layer
------------------------------------------

Most developers reach for HTTP middleware or a simple `sleep()` when they need to respect a third-party API rate limit inside a queued job. Both approaches are wrong. HTTP middleware never runs in a queue worker, and `sleep()` blocks the worker process entirely, starving every other job on that queue.

Laravel ships a first-class answer: **job middleware** combined with `Illuminate\Queue\Middleware\RateLimited`. Used correctly, it releases the job back onto the queue with a calculated delay instead of blocking or failing.

---

How `RateLimited` Works Under the Hood
--------------------------------------

The middleware wraps your job's `handle()` call. Before execution it attempts to acquire a Redis atomic counter keyed to the limiter name. If the limit is exhausted it calls `$job->release($seconds)` and returns early — the job is not marked as failed, it simply re-queues itself.

```php
// app/Jobs/SyncContactToHubspot.php
use Illuminate\Queue\Middleware\RateLimited;

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

```

The limiter itself is registered in `AppServiceProvider` (or a dedicated `RateLimiterServiceProvider`):

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

RateLimiter::for('hubspot', function (object $job) {
    return Limit::perMinute(100)->by('hubspot_global');
});

```

The closure receives the **job instance**, so you can scope limits per tenant:

```php
RateLimiter::for('hubspot', function (SyncContactToHubspot $job) {
    return [
        Limit::perMinute(100)->by('hubspot_global'),
        Limit::perMinute(20)->by('hubspot_tenant_' . $job->tenantId),
    ];
});

```

Returning an array applies **all** limits; the job is released if any one is exceeded.

---

Controlling Release Delay and Max Attempts
------------------------------------------

By default the middleware releases with a 0-second delay, causing a tight retry loop. Override it:

```php
return [new RateLimited('hubspot')->dontRelease()];
// or
return [(new RateLimited('hubspot'))->releaseAfterOneMinute()];
// or custom:
return [(new RateLimited('hubspot'))->releaseAfterBackoff($this->attempts())];

```

`releaseAfterBackoff()` uses the job's own `$backoff` property, keeping retry logic in one place.

Because a release does **not** increment the attempt counter, you must guard against infinite loops explicitly:

```php
public int $tries = 10;
public int $maxExceptions = 3;

```

Or use `$this->release()` with a hard cap inside the middleware itself by subclassing:

```php
namespace App\Queue\Middleware;

use Illuminate\Queue\Middleware\RateLimited as Base;

class RateLimitedWithCap extends Base
{
    public function handle(mixed $job, callable $next): void
    {
        if ($job->rateLimitedReleases >= 50) {
            $job->fail(new \RuntimeException('Rate limit cap exceeded'));
            return;
        }

        parent::handle($job, function ($j) use ($next) {
            $next($j);
        });
    }
}

```

Track `$rateLimitedReleases` as a public property on the job so it survives serialization.

---

Composing Multiple Middleware
-----------------------------

Job middleware stacks are just arrays. Combine rate limiting with deduplication and exception throttling:

```php
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\Middleware\ThrottlesExceptions;

public function middleware(): array
{
    return [
        new WithoutOverlapping($this->contactId),
        new RateLimited('hubspot'),
        (new ThrottlesExceptions(5, 10))->backoff(2),
    ];
}

```

`WithoutOverlapping` prevents duplicate in-flight jobs for the same contact. `ThrottlesExceptions` backs off when the API starts returning 5xx errors. `RateLimited` enforces the quota. Each concern is isolated.

---

Testing Rate-Limited Jobs
-------------------------

In Pest, fake the rate limiter to assert release behaviour without a real Redis connection:

```php
use Illuminate\Support\Facades\RateLimiter;

it('releases the job when the hubspot limit is hit', function () {
    RateLimiter::for('hubspot', fn () => Limit::perMinute(0)); // always exhausted

    $job = new SyncContactToHubspot(tenantId: 1, contactId: 42);
    $job->handle(); // should not throw

    // Assert job was released, not failed
    expect($job->isReleased())->toBeTrue();
});

```

For a true integration test, use `Queue::fake()` and dispatch the job, then assert it was pushed back with a delay.

---

Takeaways
---------

- Use `RateLimited` middleware, not `sleep()`, to throttle jobs without blocking workers.
- Scope limiters to tenants or resource IDs by inspecting the job instance in the closure.
- Return an array of `Limit` objects to enforce both global and per-tenant quotas simultaneously.
- Always set `$tries` or a custom release cap to prevent infinite release loops.
- Compose `WithoutOverlapping`, `RateLimited`, and `ThrottlesExceptions` for production-grade resilience.

- [laravel](https://www.msaied.com/articles?search=laravel)
- [queues](https://www.msaied.com/articles?search=queues)
- [redis](https://www.msaied.com/articles?search=redis)
- [job-middleware](https://www.msaied.com/articles?search=job-middleware)

 Frequently asked questions 
---------------------------

  Does releasing a job via RateLimited middleware count as a failed attempt?No. `$job-&gt;release()` re-queues the job without incrementing the attempt counter. You must set a `$tries` limit or track releases manually to prevent an infinite loop.

   Can I use RateLimited middleware without Redis?The built-in `RateLimited` middleware relies on Laravel's cache-backed rate limiter. Any cache driver works, but Redis is strongly recommended in production because atomic increment operations are reliable and performant under concurrent workers.

   How do I apply different rate limits per tenant in a multi-tenant app?Register your limiter with a closure that receives the job instance, then return a `Limit` keyed by the tenant ID: `Limit::perMinute(20)-&gt;by('hubspot\_tenant\_' . $job-&gt;tenantId)`. You can return an array of limits to enforce both global and per-tenant caps at once.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com#person)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](https://www.msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Telescope Alternatives: Structured Observability with Pulse, Debugbar, and Custom Watchers](https://www.msaied.com/articles/laravel-telescope-alternatives-structured-observability-with-pulse-debugbar-and-custom-watchers)  

   On this page
-------------

1. [The Problem: Throttling at the Right Layer](#the-problem-throttling-at-the-right-layer)
2. [How RateLimited Works Under the Hood](#how-coderatelimitedcode-works-under-the-hood)
3. [Controlling Release Delay and Max Attempts](#controlling-release-delay-and-max-attempts)
4. [Composing Multiple Middleware](#composing-multiple-middleware)
5. [Testing Rate-Limited Jobs](#testing-rate-limited-jobs)
6. [Takeaways](#takeaways)

 ###  Have a technical challenge?

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

[Start a conversation](https://www.msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/737/f5c7e78a645325d4cff8d43f2835f136.png)  · 4 min read### Laravel Telescope Alternatives: Structured Observability with Pulse, Debugbar, and Custom Watchers

4 Oct 2026 ](https://www.msaied.com/articles/laravel-telescope-alternatives-structured-observability-with-pulse-debugbar-and-custom-watchers) [ ![](https://cdn.msaied.com/736/98f6737af2ba4cdcc7400bc7fcab2439.png)  · 3 min read### Laravel 13: New Features, Helpers, and Upgrade Notes

4 Oct 2026 ](https://www.msaied.com/articles/laravel-13-new-features-helpers-and-upgrade-notes) [ ![](https://cdn.msaied.com/735/2b11b2815a5984a8e3ab654b67f9af60.png)  · 4 min read### Laravel New in 12: First-Class Typed Config, Fluent Routing, and Upgrade Notes

3 Oct 2026 ](https://www.msaied.com/articles/laravel-new-in-12-first-class-typed-config-fluent-routing-and-upgrade-notes) 

  Have a technical challenge?
----------------------------

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

 [Discuss your project ↗](https://www.msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/sitemap.xml)
