---
title: "Deferrers & queued jobs — HTTP Client Rate Limits | Roundly"
description: "How the wait is performed — a millisecond sleep by default, or releasing a queued job back onto the queue."
url: https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel/deferrers
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[HTTP Client Rate Limits for Laravel](https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel.md)

# 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)):

```php
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:

```php
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');
    }
}
```

The job is released at most once per deferrer. Under Http::retry(), each retry re-runs the limit, and a further defer only throws again with the first release’s delay — so the database queue never gets a second copy of the job.

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:

```php
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:

```php
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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/http-client-rate-limits-for-laravel.md)
