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

Define reusable limits once in config/http-client-rate-limits.php and reference them by name from anywhere:

// config/http-client-rate-limits.php
'limiters' => [
    'github' => ['rate' => 5, 'per' => 'second'],
    'billing' => ['rate' => 100, 'per' => 'minute', 'by' => 'tenant-1', 'jitter' => 50],
],
use RoundlyConsulting\HttpClientRateLimits\Facades\RateLimits;

Http::rateLimit('github')->get('https://api.github.com/user');

// Re-key a profile per owner at the call site:
Http::rateLimit('github', by: 'acct-1')->get('https://api.github.com/user');

// The same profile outside the HTTP client:
$limit = RateLimits::profile('github');
$limit->remaining();

Referencing a name that isn’t defined throws UnknownLimiterProfileException.

Without by, a profile is keyed by its own name, so each profile keeps its own budget and its own server backoff — one API asking you to slow down never holds up the others. Give profiles the same by to make them share one budget.

Profile keys

Each profile array accepts rate and per, plus the optional by, trim, max_wait, jitter and adaptive keys. A key that is not set — omitted, null or blank ('', whitespace) — falls back to these defaults:

KeyDefaultPurpose
rate1Requests allowed in the window.
perminuteWindow: second, minute, hour or day.
bynull (the profile name)Owner key the budget is tracked under. Profiles with the same by share one budget.
trimfalseDrop the owner’s hits older than the window after each allowed request.
max_waitnullMax wait in ms; a longer wait throws RateLimitExceededException.
jitter0Up to this many ms of random extra wait — never shortens a wait.
adaptivefalseSelf-tune from Retry-After / X-RateLimit-* response headers.

A profile using every key:

'limiters' => [
    'openai' => [
        'rate' => 60,          // requests allowed in the window
        'per' => 'minute',     // second | minute | hour | day
        'by' => 'openai',      // owner key
        'trim' => true,        // drop hits older than the window after each request
        'max_wait' => 10_000,  // ms: throw RateLimitExceededException above this wait
        'jitter' => 100,       // ms: up to this much random extra wait
        'adaptive' => true,    // honour Retry-After / X-RateLimit-* response headers
    ],
],

Strict values

Every profile value is read strictly when the profile is resolved, never guessed at:

  • rate, max_wait and jitter take an integer or an integer string ('5'); 'five', '5.5' or '1e3' throws InvalidConfigurationException, and so does a negative max_wait or jitter. A rate below 1 throws InvalidLimitException.
  • per takes second, minute, hour or day (exact) or a Timespan; a typo such as minutes, or an unsupported week, throws InvalidTimespanException.
  • by takes a string; a non-string throws InvalidConfigurationException.
  • trim and adaptive take true/false, 1/0, on/off or yes/no; anything else throws InvalidConfigurationException.

Every error names the full key, e.g. http-client-rate-limits.limiters.github.adaptive. A key that is not set uses its default from the table above — so a blank max_wait means no ceiling, never a 0 ms fail-fast one.

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.