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

Laravel Guzzle Throttle Laravel Package

hamburgscleanest/laravel-guzzle-throttle

Laravel Guzzle Throttle adds configurable rate limiting to outgoing Guzzle requests using Laravel's cache. It helps prevent API throttling errors by limiting requests per host, with simple middleware integration and customizable limits.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the wrapper and its middleware dependency with Composer:

composer require hamburgscleanest/laravel-guzzle-throttle

The service provider is auto-discovered on Laravel 5.5+. Publish the config file first — this is the only file you should touch before writing any code:

php artisan vendor:publish --provider="HamburgScCleanest\GuzzleThrottle\GuzzleThrottleServiceProvider"
# or, if the tag isn't obvious:
php artisan vendor:publish --tag=guzzle-throttle

Open config/guzzle-throttle.php. Everything you need to reason about this package lives in that one array. Skim it in this order:

  1. Cache — where the throttle's counters and timestamps are stored. Defaults to a Laravel cache store; leave it on redis or memcached in production, never array outside tests.
  2. Hosts / default — per-host limits expressed as max_requests per request_interval (seconds), with a default block for anything unlisted.
  3. Strategy / retry — how the middleware behaves when the limit is hit (block-and-wait vs. throw) and how many retries it will attempt.

First use case that justifies the install: calling a third-party API that enforces per-second or per-minute quotas — Sendgrid, Mailchimp, Twitter, GitHub, HubSpot, a partner feed, or an internal service that will 429 you if you batch jobs too aggressively. Instead of sprinkling sleep() calls through your queue jobs, you configure the host once and every GuzzleThrottle::* call from your app is automatically serialized against that budget.

Minimal first call:

use GuzzleThrottle;

$response = GuzzleThrottle::get('https://api.partner.com/v1/users', [
    'headers' => ['Authorization' => 'Bearer '.config('services.partner.token')],
]);

$data = $response->json();

That is the whole API surface for the happy path — the facade proxies the standard Guzzle request methods (get, post, put, patch, delete, request, sendAsync) but routes them through the throttling middleware.

Implementation Patterns

Wrap, don't replace, existing clients. If you already have a configured GuzzleHttp\Client (base URI, auth middleware, retry middleware), the cleanest integration is to inject the package's throttling middleware into that client rather than using the facade. The facade is convenient for one-off calls but doesn't know about your existing handler stack. For anything nontrivial, resolve the middleware from the container and attach it in your own client factory or service provider.

One service class per external API. Put the throttled client behind a small PartnerApiClient class injected through the container. Your controllers, jobs, and listeners then depend on PartnerApiClient::users(), not on GuzzleThrottle::get() directly. This keeps the rate-limit config and the API contract in one place and makes it trivial to mock in tests.

class PartnerApiClient
{
    public function __construct(private Client $http) {}

    public function users(int $page): array
    {
        return $this->http
            ->get("/v1/users?page={$page}")
            ->json();
    }
}

Use it from queued jobs without extra locking. The throttle state lives in the cache, not in the PHP process, so N workers hitting the same host share one counter. You do not need WithoutOverlapping or a RateLimited job middleware for the API-side limit — that concern is already solved. Do still keep queue concurrency sane for other reasons (DB connections, memory).

Size the config to the API's published limits, rounded down. If the API says "600 requests/minute", set max_requests => 550 and request_interval => 60 — not 600. You want headroom for retries, clock skew on cache TTLs, and bursty job dispatch. A 5–10 % margin costs almost nothing and eliminates the tail of 429s at the minute boundary.

Batch and paginate deliberately. The throttle prevents you from being rejected, not from being slow. A job that loops over 50,000 rows at 10 req/s will still take ~1.4 hours. Design for it: paginate in chunks, dispatch a follow-up job per chunk, and let the queue absorb the wall-clock time. The throttle makes the chunk job safe to parallelize — up to the configured limit.

Combine with an application-level circuit breaker. When the upstream returns 5xx consistently, the throttle will happily keep feeding it requests at full speed because the rate limiter only cares about volume, not health. Layer a simple breaker (fail fast after N consecutive errors, back off via cache flag) on top so a degraded upstream isn't hammered until the throttle window resets.

Testing: swap to the array cache and lower limits. In phpunit.xml or TestCase::setUp, point the throttle cache to the array driver and set max_requests => 2, request_interval => 1. Then a handful of sequential calls in a test will exercise the wait path deterministically without slowing the suite. Assert on the number of Guzzle history entries when you need to prove that a retry happened.

Gotchas and Tips

The cache is the throttle. If two environments share a Redis instance (staging and prod on the same cluster, e.g.), they share counters. Prefix the cache key per environment or use separate databases. Also: if someone flushes the app cache as part of a deploy hook, you lose your in-flight counters — usually harmless, but be aware that a deploy resets the budget mid-window.

array cache in production is a silent bug. It works locally and looks correct, but every PHP process gets its own counter, so N workers send N× the intended request rate. This is the most common way people "use the package and it doesn't work." Verify the driver in the config matches your real Laravel cache store, especially after moving from local to multi-worker queue deployments.

Cache TTL must exceed your request_interval. If the cache evicts the request log before the window closes, the middleware sees an empty log and immediately fires — effectively no limit. On Redis with an LRU policy under memory pressure this can happen silently. Either give the throttle its own store or set a conservative maxmemory policy.

Retry count and blocking time are multiplicative. retry_count × request_interval is the worst-case latency for a single request. With retry_count => 10 and a 60-second interval, one HTTP call can block a queue worker for ten minutes. Set retry_count to something like 2–3 and handle the "genuinely overloaded" case in the calling code, so you fail fast instead of parking a worker.

Config is cached. After editing config/guzzle-throttle.php on a production box, run php artisan config:cache. Conversely, if you're debugging and your config changes don't take effect, check whether the config is cached before assuming the package is broken.

The facade resolves a fresh client per call. GuzzleThrottle::get(...) is fine for ad-hoc calls but you don't get connection reuse or a shared Guzzle handler/keep-alive pool across N calls. For tight loops (pagination, backfills), build one client with the middleware attached and reuse it — same throttling behavior, materially better throughput.

Debugging a "why is this so slow?" complaint. The most useful single signal is the throttle state in the cache: list keys matching the throttle prefix and read their TTL and value. If the TTL is near the full request_interval, you're constantly at the ceiling and the limit is simply too low for the workload. If it's usually near zero, something else (DNS, TLS, upstream latency) is the bottleneck. Don't guess — look at the cache.

Extension points worth knowing. The underlying guzzle-advanced-throttle middleware accepts a rate-limiter strategy and a custom key resolver. If your API's limits are per-endpoint rather than per-host, or if you need a token-bucket (bursty) model instead of a fixed window, that's the layer to extend — subclass the strategy and wire it into the config, rather than post-processing with sleep() after the fact. Keep custom strategies covered by the array-cache test described above; they're the easiest part of this stack to get subtly wrong.

Version drift between the wrapper and the underlying middleware. The Laravel package is a thin shim over guzzle-advanced-throttle and guzzlehttp/guzzle. Major bumps of Guzzle (6 → 7) have historically broken these wrappers. Pin the Guzzle major version you've tested against in composer.json, and don't let a routine composer update land a new Guzzle major alongside this package without running the throttle tests.

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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox