spatie/guzzle-rate-limiter-middleware
A Guzzle middleware that enforces rate limits by specifying max requests per minute or second. When the limit is reached, requests sleep until allowed. Includes an InMemoryStore and supports custom persistent stores for cross-process rate limiting.
Partner/API quota compliance as a shipping gate. Any integration with a documented "N requests per minute" contract is a hard product constraint, not an implementation detail. This package turns that contract into one line of configuration (RateLimiterMiddleware::perMinute(60)) sitting next to the credentials in config/services.php, which means the limit becomes reviewable in a PR instead of tribal knowledge. That directly supports roadmap decisions like "can we ship the bulk sync this quarter" — the answer usually depends on whether you can stay inside a quota, not on whether you can write the loop.
Build vs. buy: buy, but narrowly. The logic is small enough to write in an afternoon, but the two things you'd get wrong are the ones that bite in production: the pluggable Store interface that lets the request history survive across processes, and the Deferrer seam that lets you swap usleep for something observable. Reimplementing costs a sprint of engineering plus a permanent maintenance line; adopting costs a Composer dependency, MIT-licensed, with no config files and no service provider to own. At 169 stars and zero dependents it is a tactical utility, not a platform bet — treat it as such in the buy/build ledger.
Architecture decisions about where outbound calls are allowed to block. The package's core behavior is sleep in the PHP process. That is a fine trade in a queue worker and an unacceptable one in a synchronous request behind a user's spinner. Forcing that distinction into the open early shapes real product choices: which operations are async-only, whether a "sync now" button exists at all, and how much latency budget a given feature gets. It's the kind of decision that's cheap before the first endpoint ships and expensive after.
Multi-worker scaling math. The bundled InMemoryStore gives every PHP process its own full quota — four queue workers means four times the limit, which means you trip the partner's limiter anyway and the tool silently failed. The decision this forces is infrastructural: Redis- or DB-backed Store before horizontal scaling, not after. That's a capacity-planning input, not a code detail.
Retry and backoff strategy as a paired decision. Pacing prevents 429s; it does nothing about the one that arrives anyway from clock skew or another consumer sharing the API key. So this package is only half a reliability story — it should be adopted alongside retry middleware, and the limit sized with retries in mind (a 429 storm drains the pacing budget faster than steady-state traffic). This is a "define the whole policy" decision rather than a "add a library" decision.
Use cases worth scoping: queued jobs and artisan commands pulling from partner APIs; scheduled syncs against rate-limited SaaS endpoints (CRM, ERP, payment, logistics); webhook fan-out where the outbound side has a cap; multi-tenant background processing where one tenant's burst must not starve another's quota.
Instrumentation as a first-class requirement. There is no event and no log line when the limiter sleeps, and silent multi-second stalls are precisely what you want on a dashboard. The Deferrer interface (v2) is the only hook. If you adopt this, budget for a decorating Deferrer that emits a metric — otherwise the first "why is the nightly sync slow" investigation starts from zero.
Good fit when:
Http facade in a way that lets you inject a HandlerStack.Store (Redis/DB) because you run more than one process. If you can't, don't adopt — you'll get the illusion of protection without the substance./search at 5/min and /bulk at 1/min means two clients, two stacks, two store keys. If your quota model is granular, you're building a layer above this regardless.Look elsewhere when:
X-RateLimit-Remaining / Retry-After headers. This package is a fixed local budget; it does not read response headers. Header-driven throttling is a different, and often better, design for well-behaved APIs.usleep in an FPM worker is a queue of stalled workers; prefer a token-bucket check that fails fast plus a queued retry.Adoption caveats to write into the ticket: pin ^2.2 if you're on Guzzle 8 — earlier versions type-error against it. Bind the client (or HandlerStack) as a singleton so the store and its history survive across calls in a worker; scattering HandlerStack::create() across the codebase silently multiplies your quota. Order matters with retries: the limiter must sit inside the retry middleware so every attempt counts.
To executives — one paragraph: This is a small, free (MIT), well-maintained library from Spatie that stops our outbound API calls from exceeding partner-imposed request quotas. Without it, a burst of background traffic can get our integration throttled or our API key suspended — a customer-visible outage with an external cause and a slow remediation path. With it, the quota lives in config next to the credentials, is reviewable in code review, and enforcement is automatic. The cost is a single Composer dependency and roughly a day of engineering to wire correctly, including a shared cache so it works across our queue workers. It is a tactical reliability control, not a strategic platform commitment.
To engineering — the shape of the work: One configured Guzzle client per external API, bound as a singleton in the service container, with the limit read from config/services.php. Implement the two-method Store interface against Redis or the cache before we scale past one worker — the bundled in-memory store gives each process its own full quota, so N workers means no limiting at all. Prune the timestamp array in push() using the $limit argument (that's why v2 added it) and set a TTL. Pair it with retry middleware for 429/503, ordered so the limiter sits inside the retry so every attempt is counted. Decorate SleepDeferrer to log and emit a metric, because the package emits nothing on its own. Scope it to queued jobs and console commands only — no blocking sleeps in web request paths. Where an API exposes Retry-After or X-RateLimit-Remaining, treat that as the authority and this as the floor.
The honest caveat to state up front: this is a 169-star utility, not infrastructure with a support contract. It solves one problem well — fixed local pacing for Guzzle clients — and deliberately solves nothing else. No header awareness, no per-endpoint quotas, no observability. Value lands only if we do the store, the ordering, and the instrumentation around it; the library itself is the smallest part of the adoption.
How can I help you explore Laravel packages today?