alama@world: ~/
./toggle-theme
cat blog/oss-adrs-laravel-rate-limited-job-middleware.md

OSS-ADR: Spatie Rate Limited Job Middleware

· post · energy

Understanding queue architecture, Redis throttles, and Leaky Bucket algorithms in Spatie's Job Middleware.

1. Context & Decision

The Problem: When interacting with third-party APIs (like Twitter, GitHub, or Shopify) through asynchronous Laravel queues, you inevitably hit rate limits. If you process 500 queued jobs per minute but the API only allows 60 requests per minute, the API will start returning HTTP 429 errors. Your jobs will fail, clog your failed_jobs table, and potentially get you banned from the API.

The Alternatives: - Catching HTTP 429 exceptions inside the job and calling $this->release(60). (Clutters business logic). - Using Redis::throttle manually inside every job's handle() method. (A lot of boilerplate). - Creating custom queue workers or delayed dispatches.

The Decision: We chose spatie/laravel-rate-limited-job-middleware because it cleanly separates rate-limiting infrastructure from the job's business logic using Laravel's Job Middleware pattern. The job itself remains completely unaware of the API limits, keeping the code clean and focused.

2. The Integration

The Quick Win: To rate-limit a job, you don't touch the handle method. Instead, you define a middleware method on the job class and return the Spatie middleware configured with a fluent API:

use Spatie\RateLimitedMiddleware\RateLimited;

class SyncUserToHubspotJob implements ShouldQueue
{
    public function middleware()
    {
        return [
            (new RateLimited())
                ->allow(10)          // Allow 10 jobs...
                ->everySeconds(60)   // ...every 60 seconds
                ->releaseAfterSeconds(30) // If limited, wait 30s before retrying
        ];
    }

    public function handle()
    {
        // Pure business logic here!
        Hubspot::sync($this->user);
    }
}

Practical Usage: The middleware intercepts the job before it executes. If the limit is reached, it automatically releases the job back onto the queue for later, without marking it as a failure.

3. Under the Hood

Architecture & Design Patterns: Laravel allows objects to be passed into a job's middleware() array. Before a job executes, Laravel pushes the job through these middleware classes (similar to HTTP middleware).

Inside the spatie-laravel-rate-limited-job-middleware source code, the handle($job, Closure $next) method determines if the job can proceed. It supports two backends: 1. Redis: Uses Laravel's native Illuminate\Redis\Limiters\DurationLimiter via Redis::throttle(). 2. Cache: If Redis isn't available, it gracefully falls back to a Leaky Bucket algorithm (ArtisanSdk\RateLimiter\Buckets\Leaky) using the standard Cache store!

When a job is blocked, the middleware fires a LimitExceeded event (great for monitoring) and releases the job: $job->release($this->releaseDuration()).

Code Masterclass: Preventing Integer Overflow in Exponential Backoff One of the coolest features is the releaseAfterBackoff($attemptedCount, $backoffRate) method. It calculates an exponential backoff time (e.g., waiting 2 seconds, then 4, then 8, then 16) before retrying the job.

However, exponential math in PHP can quickly lead to integer overflow errors. Spatie elegantly handles this by capping the calculation:

public function releaseAfterBackoff(int $attemptedCount, int $backoffRate = 2): static
{
    $releaseAfterSeconds = 0;
    $interval = $this->releaseInSeconds;
    $maxSeconds = 86400 * 365; // Cap at 1 year

    for ($attempt = 0; $attempt <= $attemptedCount; $attempt++) {
        if ($attempt > 30) { // 2^30 is already over a billion
            $releaseAfterSeconds = $maxSeconds;
            break;
        }

        $power = pow($backoffRate, $attempt);
        $increment = $interval * $power;

        // Prevent overflow
        if ($releaseAfterSeconds > $maxSeconds || $increment > $maxSeconds - $releaseAfterSeconds) {
            $releaseAfterSeconds = $maxSeconds;
            break;
        }

        $releaseAfterSeconds += $increment;
    }

    // ...
}

This defensive programming ensures that no matter how many times a job fails, the release calculation will safely max out at 1 year without crashing the PHP worker process.

4. Consequences & Trade-offs

The Good: - Separation of Concerns: Business logic is entirely decoupled from API constraint logic. - Resilience: Gracefully handles unexpected bursts of traffic without throwing exceptions.

The Gotchas: - Queue Clogging: If you allow 10 jobs per minute, but dispatch 10,000 jobs instantly, those jobs will repeatedly cycle back into the queue for hours. This can clog your worker and delay other important jobs. You should route rate-limited jobs to a dedicated queue queue (e.g., php artisan queue:work --queue=api-sync,default).

#oss #architecture #spatie #laravel