NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Ask the limiter about the current state before sending — without recording a hit:

use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

$limit = RateLimits::perMinute(30)->by('acct-1');

$limit->remaining();        // requests still allowed in the window (0 during a server penalty)
$limit->availableIn();      // ms until the next request is allowed (0 = now)
$limit->tooManyAttempts();  // bool: is the window exhausted right now?

Delay at a given timestamp

RateLimit forwards calls to the underlying Limit and Limiter, so you can ask how long you’d have to wait at a specific moment (in milliseconds, from the deferrer’s clock):

$middleware = RateLimits::perHour(6);

$at    = $middleware->getDeferrer()->timestamp();
$delay = $middleware->delayUntilNextRequestInMs($at); // 0 = send now, else wait this many ms

$middleware->isOverMaxAttempts(7); // true

Getters

$limit = RateLimits::perMinute(30)->by('acct-1');

$limit->getKey();               // 'acct-1'
$limit->getMaxAttempts();       // 30
$limit->getTimespan();          // 'minute'
$limit->isUnderMaxAttempts(29); // true
$limit->getStore();             // the Store in use
$limit->getDeferrer();          // the Deferrer in use
$limit->getLimiter();           // the underlying Limiter

Rescheduling instead of waiting

Inspection reads the store, so in queued jobs pair it with a shared store. A job can then release itself instead of waiting:

public function handle(): void
{
    // Needs a shared store (cache, Redis or database) so every run reads the same budget.
    $limit = RateLimits::profile('github')->by("acct:{$this->accountId}");

    if ($limit->tooManyAttempts()) {
        $this->release((int) ceil($limit->availableIn() / 1000));

        return;
    }

    Http::withMiddleware($limit)->get('https://api.github.com/user');
}

Inspection methods describe the primary window (plus any adaptive penalty on it) and record nothing; compound windows are evaluated when the request is sent.

Resetting a limit

Clear the hits a key has recorded — after a plan upgrade, or between two phases of a batch — so the next request goes straight through:

use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

RateLimits::perMinute(60)->by('stripe')->reset();

// A compound limit clears every window it enforces.
RateLimits::perSecond(5)->by('stripe')->alongside(RateLimits::perMinute(100)->by('stripe'))->reset();

A reset clears the windows the limit enforces (stripe:minute in the first line), not other windows on the same key, and it doesn’t lift a penalty an adaptive limit recorded from the server’s own Retry-After — the server asked for that wait. It fires RateLimitReset.

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

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