Compound limits
Pass an array of limits to enforce them all on one request. The limiter defers to the strictest window and records a hit on every window when the call is allowed:
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
Http::rateLimit([RateLimits::perSecond(5), RateLimits::perMinute(100)])
->get('https://api.example.com/things');
// Or fluently, alongside the primary window:
$middleware = RateLimits::perSecond(5)->alongside(RateLimits::perMinute(100));
// Or by stacking the macro:
Http::rateLimit(RateLimits::perSecond(5))->rateLimit(RateLimits::perMinute(100));
// `by:` scopes every window — here both acct-1:second and acct-1:minute.
Http::rateLimit([RateLimits::perSecond(5), RateLimits::perMinute(100)], by: 'acct-1')
->get('https://api.example.com/things');Each window keeps its own count in the store — under {key}:{window}, e.g. global:second and global:minute (see Limit::storeKey()) — so a request counts exactly once per window however the limits are combined, and trimming the short window never erases the long one. Limits that share both key and window share one budget, wherever they are built. The macro’s by: argument scopes every window.
Atomic across windows
A compound limit (an array, compound() or alongside()) checks and records all its windows in one atomic store step. Stacked macros are two separate limiters: each is atomic on its own windows, but the first records its hit before the second decides whether to wait — prefer the compound form when that matters. Http::rateLimit($limit, by: '...') re-keys a copy, so a RateLimit or Limit you pass in keeps its own key.
More windows & the facade
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;
// alongside() also takes a list of Limit / RateLimit entries:
$middleware = RateLimits::perSecond(5)->by('api')->alongside([
RateLimits::perMinute(100)->by('api'),
RateLimits::perDay(5_000)->by('api'),
]);
// Or build it through the facade:
$middleware = RateLimits::compound([
RateLimits::perSecond(5)->by('api'),
RateLimits::perMinute(100)->by('api'),
]);A maxWait() or jitter() set on any window of a compound limit applies to the whole wait — the tightest ceiling and the widest jitter win — whichever window turns out to be the bottleneck. The inspection methods (remaining(), availableIn(), tooManyAttempts()) report on the primary window (plus any adaptive penalty on it); reset() clears every window the limit enforces.
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.