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

Opt in with ->adaptive() and the limiter reads Retry-After and X-RateLimit-Remaining / X-RateLimit-Reset from each response, recording a penalty in the store so the next request waits exactly as long as the server asked:

use Illuminate\Support\Facades\Http;
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

Http::rateLimit(RateLimits::perSecond(20)->adaptive())
    ->get('https://api.example.com/things');

It works the same through Http::rateLimit(), Http::withMiddleware(), Http::pool(), an adaptive named profile and handle() around any callable that returns a response:

$api = RateLimits::perSecond(20)->by('api')->adaptive();

// Wrap any callable that returns a response; its headers are read the same way.
$response = $api->handle(fn () => Http::get('https://api.example.com/things'));

// After a 429 with "Retry-After: 7", the next call through $api waits 7 seconds.
$next = $api->handle(fn () => Http::get('https://api.example.com/things'));

How the headers are read

  • Retry-After — delta-seconds or an HTTP-date; it takes precedence when present.
  • Otherwise, X-RateLimit-Remaining: 0 together with a numeric X-RateLimit-Reset. The reset may be a delay in seconds or an epoch timestamp; an epoch that has already passed asks for no wait.
  • Headers that don’t ask for a backoff — remaining above zero, a missing or non-numeric reset — are ignored.
  • A server wait is honoured up to one day: a larger Retry-After / X-RateLimit-Reset (hostile or broken) is capped there rather than stalling the worker or overflowing.
  • The penalty is stored with the store’s penalizeUntil() under every adaptive window’s owner key. On a shared store, every process using that key backs off together.

From a named profile

// config/http-client-rate-limits.php
'limiters' => [
    'partner' => ['rate' => 20, 'per' => 'second', 'by' => 'partner', 'adaptive' => true],
],

// anywhere
Http::rateLimit('partner')->get('https://partner.example.com/feed');

Adaptive limiting complements the proactive pacing — the declared rate still applies, and the server’s signal extends the wait when it asks for more.

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.