Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rate Limit Plugin Laravel Package

saloonphp/rate-limit-plugin

A Saloon plugin for respectful API clients: it tracks requests and handles rate-limit responses, retrying or delaying calls when limits are reached. Drop it into Saloon's middleware pipeline for simple, reliable rate-limit management.

View on GitHub
Deep Wiki
Context7

Getting Started

The rate-limit plugin is a Saloon add-on: it adds client-side throttling to a Saloon connector so you stop hammering an upstream API before it starts returning 429s (or, worse, banning you). It is not a Laravel rate limiter replacement — it complements RateLimiter/throttle middleware by tracking outbound calls.

Minimal setup:

composer install saloonphp/rate-limit-plugin
use Saloon\Http\Connector;
use Saloon\RateLimitPlugin\Traits\HasRateLimits;
use Saloon\RateLimitPlugin\Limit;
use Saloon\RateLimitPlugin\Contracts\RateLimitStore;
use Saloon\RateLimitPlugin\Stores\LaravelCacheStore;

class GitHubConnector extends Connector
{
    use HasRateLimits;

    public function resolveBaseUrl(): string
    {
        return 'https://api.github.com';
    }

    protected function rateLimits(): array
    {
        return [
            Limit::allow(5000)->everyHour(),
            Limit::allow(60)->everyMinute()->for('search'),
        ];
    }

    protected function resolveRateLimitStore(): RateLimitStore
    {
        return new LaravelCacheStore(); // pass a store name for a dedicated driver
    }
}

Nothing else in your codebase changes: $connector->send($request) behaves as before, except that when a limit is exhausted a RateLimitReachedException is thrown before the request leaves your app.

Where to look first:

  • Saloon\RateLimitPlugin\Limit — the fluent builder (allow(), everySecond/Minute/Hour/Day(), scoping).
  • Saloon\RateLimitPlugin\Contracts\RateLimitStore — the small interface you implement to back limits with anything (Redis, DB, DynamoDB).
  • Saloon\RateLimitPlugin\Stores\LaravelCacheStore — the one you'll actually use in Laravel.
  • Saloon\RateLimitPlugin\Exceptions\RateLimitReachedException — what you catch in jobs/controllers.

First use case: a third-party API with a documented quota (GitHub 5000/hr, Stripe ~100 reads/sec, HubSpot burst limits) where you already have a Saloon connector. Add the trait, declare the documented numbers, point the store at Redis, done — that's a 10-line change that removes an entire class of production incidents.

Implementation Patterns

Layered limits on one connector. Declare every documented window at once; all limits are evaluated and the most restrictive wins. Typical: a burst limit (60/second) plus a daily cap (10000/day).

Scoped/named limits for endpoint-specific quotas. Search endpoints, bulk endpoints, and write endpoints usually have separate budgets. Give each its own scope name (e.g. ->for('search')) so a search-heavy job doesn't starve your normal reads. Keep the names in constants or an enum — stringly-typed scopes rot fast.

Shared store across queue workers. This is the single most important pattern. With a per-process store, four queue workers each think they have the full budget. Wire LaravelCacheStore to Redis/Memcached (or database/file if you accept weaker atomicity) and the limit becomes cluster-wide.

Base connector + inheritance. Put the trait and store resolution on an abstract VendorConnector, then let per-service connectors override rateLimits() and merge parent limits. Keeps the store config in one place.

Catch and reschedule in jobs. The exception is thrown before dispatch, so nothing is consumed upstream — ideal for a delayed retry:

try {
    $connector->send(new SyncOrders($account));
} catch (RateLimitReachedException $e) {
    $limit = $e->getLimit();
    // seconds remaining in the current window
    $this->release($secondsUntilReset + 1);
}

Use release() rather than delete() so you don't burn queue attempts, and make the job idempotent.

Pre-flight checks for user-facing flows. If a user clicks a button and you'd rather show "try again in 40s" than throw, inspect the remaining budget on the connector before sending and short-circuit with a friendly response.

Sync from upstream headers. For APIs that return X-RateLimit-Remaining/Retry-After, you can reconcile your local counter with reality (via a custom store, or by reading the response in a Saloon response middleware/Response hook) so limits stay accurate even when other systems hit the same account.

Testing. Swap in ArrayStore in your test suite so state is per-test and never pollutes Redis; assert that the Nth+1 call throws rather than asserting on the number of faked HTTP calls.

Gotchas and Tips

  • The default store is not shared. If you don't implement resolveRateLimitStore(), limits live in process memory and reset on every request — in Octane/FPM that means the limiter effectively does nothing. Configure it explicitly and assert it in a test.
  • Cached responses now count (v2.5.3). A fix landed so responses served from Saloon's caching plugin are counted against HasRateLimits. Previously a cache hit skipped the limiter entirely, letting a mixed cached/live workload drift past the real quota. Behaviourally: if you cache aggressively and rate limit on the same connector, cache hits will now consume budget. If that's not what you want, either use a separate connector without the trait for cached reads, or scope the cached endpoints to their own named limit.
  • The exception is not a RequestException. It's thrown before a pending request is sent, so catch (RequestException $e) won't catch it. Catch RateLimitReachedException explicitly (or SaloonException) — otherwise it bubbles out of a job as an unhandled failure.
  • Failed requests still consume budget. The limiter counts attempts, not successes. A flaky endpoint returning 500s will eat your window; combined with Saloon's retry plugin, a 3-attempt retry costs 3 slots. Order your plugins/middleware deliberately and consider disabling retries for limit-sensitive endpoints.
  • Atomicity depends on your cache driver. Redis/Memcached increments are atomic; file and database (without locking) can race under concurrency. Don't rely on tight windows on weak drivers.
  • Fixed windows, not sliding. Budget resets when the key's TTL expires, so you can burst 2× the limit across a window boundary. Size limits with that in mind, or build a sliding window into a custom store.
  • Cache key collisions. Multiple apps sharing one Redis instance, or two connectors hitting the same API with different credentials, can silently share buckets. Namespace keys via the store/cache prefix, and per-account limits should include the account identifier.
  • Debugging "why am I throttled". Dump the connector's configured limits and each limit's remaining count/expiry to see which one is binding; clear the specific cache keys to reset state. Log the exception's limit name in production — the message alone rarely tells you which bucket tripped.
  • Extension points. resolveRateLimitStore() for storage, a custom RateLimitStore implementation for non-cache backends (DB table with a unique constraint, DynamoDB, a centralised limiter service), and scoped Limit names for per-endpoint policy. Those three cover almost every customisation you'll need.
  • Version drift. The API moved around across Saloon v2 → v3 (plugin registration, class naming). If snippets from a blog post don't work, check the installed minor version against the README before assuming you're misusing it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment