Deferrers & queued jobs
A Deferrer reads the current time and performs the wait when a limit is reached.
SleepDeferrer
The default. Reads the current time in milliseconds and pauses with Illuminate\Support\Sleep — which you can fake in tests (see Testing).
ReleaseDeferrer
For use inside a queued job: instead of blocking the worker, it releases the job back onto the queue with the computed delay (rounded up to whole seconds, so the job never re-runs early) and throws JobReleasedException — carrying the limit key and the job — to unwind the current attempt without sending the request. Opt in with RateLimits::releasingJob() — shorthand for RateLimits::usingDeferrer(new ReleaseDeferrer($job)):
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
// inside a queued job's handle(), $this uses Illuminate\Queue\InteractsWithQueue
$rateLimit = RateLimits::releasingJob($this)->perSecond(5);
Http::withMiddleware($rateLimit)->get('https://api.example.com/things');Give the job the HandlesRateLimitRelease middleware: it ends that attempt as a normal return, so the worker doesn’t report it, fire JobExceptionOccurred, or count it toward the job’s $maxExceptions. A complete job — with a shared store configured, so every run reads the same budget:
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
use RoundlyConsulting\HttpClientRateLimits\Jobs\Middleware\HandlesRateLimitRelease;
final class SyncThings implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
// Every release counts as an attempt: allow enough (or define retryUntil()).
public int $tries = 10;
public function middleware(): array
{
return [new HandlesRateLimitRelease];
}
public function handle(): void
{
$rateLimit = RateLimits::releasingJob($this)->perSecond(5);
Http::withMiddleware($rateLimit)->get('https://api.example.com/things');
}
}As with any job Laravel releases, each release uses up one of the job’s $tries, so give it enough of them. The middleware swallows only this job’s own release; an exception for another job keeps propagating. Without the middleware, catch the exception yourself — the job is already back on the queue:
use RoundlyConsulting\HttpClientRateLimits\Exceptions\JobReleasedException;
try {
Http::withMiddleware(RateLimits::releasingJob($this)->perSecond(5))->get($url);
} catch (JobReleasedException) {
return;
}The job must expose Laravel’s release() method (e.g. via InteractsWithQueue); otherwise releasingJob() — the ReleaseDeferrer constructor — throws InvalidDeferrerException.
Writing your own
Implement the Deferrer contract — timestamp() in milliseconds and defer(int $ms, string $key). After a defer the limiter checks the store again; a deferrer whose clock did not move on (a simulated wait) is taken at its word:
namespace RoundlyConsulting\HttpClientRateLimits\Deferrer;
interface Deferrer
{
// "Now" in milliseconds — the clock every window is measured on.
public function timestamp(): int;
// Wait $ms before the request for limit $key may go; the limiter then re-checks the store.
public function defer(int $ms, string $key): void;
}Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.