Rate limiting requests
The package adds a rateLimit() macro to Laravel’s HTTP client — the quickest way to throttle outgoing calls:
use Illuminate\Support\Facades\Http;
// Integer shorthand = N requests per minute.
$response = Http::rateLimit(30)->get('https://api.example.com/orders');When the limit is reached, the middleware waits the exact time until the next request is allowed, checks again, then proceeds — you simply get your response a little later. (If another worker on a shared store took that slot while this one waited, it waits again rather than sending over the limit.) Pass a RateLimit (built by the RateLimits facade) or a Limit to pick a different window, and by: to scope the budget to an owner:
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
Http::rateLimit(RateLimits::perSecond(5))->get('https://api.example.com/things');
Http::rateLimit(RateLimits::perDay(10_000))->get('https://api.example.com/report');
// Each owner gets its own budget (e.g. per account or outbound IP).
Http::rateLimit(30, by: 'acct-1')->get('https://api.example.com/orders');What rateLimit() accepts
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
use RoundlyConsulting\HttpClientRateLimits\Limit;
Http::rateLimit(30); // int: N requests per minute
Http::rateLimit('github'); // string: named profile from config
Http::rateLimit(RateLimits::perSecond(5)); // a RateLimit
Http::rateLimit(new Limit(maxAttempts: 100, timespan: 'hour')); // a Limit value object
Http::rateLimit([RateLimits::perSecond(5), RateLimits::perMinute(100)]); // compound windows
Http::rateLimit(30, by: 'acct-1'); // any form, scoped to an ownerThe RateLimit middleware directly
RateLimit is a Guzzle middleware, so you can also attach it with Http::withMiddleware():
$response = Http::withMiddleware(RateLimits::perSecond(5))
->get('https://api.example.com/things');Building limits
Every limit comes from the RateLimits facade (see The RateLimits facade):
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
RateLimits::make(Limit $limit); // from a Limit value object
RateLimits::perSecond(int $maxAttempts = 1); // N requests per second
RateLimits::perMinute(int $maxAttempts = 1); // N requests per minute
RateLimits::perHour(int $maxAttempts = 1); // N requests per hour
RateLimits::perDay(int $maxAttempts = 1); // N requests per day
RateLimits::profile(string $name); // a named profile from config
RateLimits::compound(array $limits); // several windows at onceEach returned RateLimit is fluent: ->by(), ->alongside(), ->maxWait(), ->jitter(), ->adaptive(), the inspection methods ->remaining(), ->availableIn() and ->tooManyAttempts(), and ->reset().
The Limit value object
A Limit holds the owner key (default global), the number of attempts, the window and the trim flag. Build one directly when you need full control, then wrap it with RateLimits::make(). Fewer than one attempt per window throws InvalidLimitException:
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
use RoundlyConsulting\HttpClientRateLimits\Limit;
// key (owner), maxAttempts, timespan (Timespan enum or 'second'|'minute'|'hour'|'day'), trim
$limit = new Limit(key: 'acct-1', maxAttempts: 100, timespan: 'hour', trim: true);
$middleware = RateLimits::make($limit);
// Limit is fluent as well:
$limit = (new Limit)->perMinute(30)->by('acct-1')->maxWait(5_000)->jitter(50);With trim enabled, the limiter drops the hits older than the window after each allowed request. Every built-in store already self-trims hits older than a day (plus an hour’s margin), so trim is optional — it keeps a key’s history down to its own window.
One budget per key and window
Every limit the manager builds records hits in one shared store, under {key}:{window} — e.g. github:second. Limits that share both key and window therefore share one budget wherever they are built, separate Http::rateLimit() calls included. The default InMemoryStore is per process only: each queue worker, PHP-FPM child or server keeps its own count, so switch to a shared store (see Stores) when several processes must draw from one budget:
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
// Same key + same window = one budget, however and wherever the limit is built.
foreach ($repositories as $repository) {
Http::rateLimit(RateLimits::perSecond(5)->by('github'))
->get("https://api.github.com/repos/{$repository}");
}
// Or build it once and reuse the instance.
$github = RateLimits::perSecond(5)->by('github');
Http::withMiddleware($github)->get('https://api.github.com/user');Rate limiting any callable
The limiter isn’t tied to the HTTP client. handle() paces any callable and returns its result:
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
$limit = RateLimits::perSecond(10)->by('sdk');
// Pace any callable (an SDK call, a raw Guzzle request, …) through the same limiter.
$result = $limit->handle(fn () => $client->createInvoice($payload));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.