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 Bundle Laravel Package

perimeter/rate-limit-bundle

perimeter/rate-limit-bundle adds configurable rate limiting to Symfony applications, helping protect APIs, controllers, and routes from abuse. Define limits, choose storage, and throttle requests with minimal setup and flexible integration.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. perimeter/rate-limit-bundle is a Symfony 2 bundle, not a Laravel package. It hooks into Symfony's HttpKernel/EventDispatcher and its service container; there is no service provider, no facade, and no Laravel integration. A composer require perimeter/rate-limit-bundle in a Laravel app will pull down Symfony 2-era component constraints and is not something you should ship. Treat this as a reference implementation to read, not a dependency to install — unless you are maintaining a legacy Symfony 2.x API, in which case it is a viable (if dated) option.

Where to look first in the repo:

  1. composer.json — confirm the Symfony version constraints and PHP floor. This tells you immediately whether it can coexist with anything you run today.
  2. Resources/config/ — the service definitions show you which storage adapters are wired and how the limiter is composed.
  3. The rate-limit annotation/attribute class and the request listener — this is the actual policy surface (what key it limits on, what window, what limit).
  4. The storage adapters — this is the part worth stealing conceptually.

First use case (Symfony 2): cap requests per API key on an /api firewall, returning 429 with a Retry-After header once the bucket is drained.

The Laravel equivalent you'd actually write:

// routes/api.php
Route::middleware('throttle:60,1')->group(function () {
    Route::get('/orders', [OrderController::class, 'index']);
});

throttle:{maxAttempts},{decayMinutes} has shipped in Laravel since 5.2. For anything beyond the trivial case, define a named limiter in AppServiceProvider::boot():

RateLimiter::for('api-key', fn (Request $request) => Limit::perMinute(120)
    ->by($request->header('X-Api-Key') ?: $request->ip()));

That is the same semantic the bundle implements, with first-party support.

Implementation Patterns

Pattern 1 — Global per-IP throttle. In the bundle this is a listener registered on kernel.request that resolves the client IP, increments the counter in the configured store, and aborts with a 429 when over budget. In Laravel, apply throttle to the api middleware group in bootstrap/app.php / Kernel.php rather than per-route; it composes with everything downstream.

Pattern 2 — Per-identity throttle (API key, tenant, user). The bundle's key resolution is IP-centric unless you supply a custom key. In Laravel this is where RateLimiter::for() with ->by(...) wins, because the key is an arbitrary closure:

RateLimiter::for('tenant', fn (Request $r) => Limit::perMinute(1000)
    ->by($r->user()?->tenant_id ?? $r->ip()));

Pattern 3 — Tiered limits. Cheap read endpoints get a generous bucket; expensive write endpoints get a tight one. In the bundle this means multiple annotated actions with different limits. In Laravel, name your limiters ('reads', 'writes') and attach them per route group — much easier to audit than annotations scattered across controllers.

Pattern 4 — Storage backend choice. This is the bundle's most transferable idea. It abstracts the counter store behind an adapter so you can swap Redis ↔ Memcached ↔ Doctrine cache. Laravel does the same thing via the cache store that RateLimiter uses — so the operational lesson carries over verbatim: use a shared, atomic store (Redis) for anything behind more than one PHP worker. Which leads directly to the gotchas.

Pattern 5 — Client-facing headers. Well-behaved rate limiting emits X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After. Laravel's ThrottleRequests middleware emits these; with the Symfony bundle you should verify whether the listener sets them and add a response listener if not, because API consumers will otherwise see bare 429s.

Integration tip for a port: if you're migrating a legacy Symfony API to Laravel and want the limits to stay identical, wrap Laravel's RateLimiter in a small service that takes the same (key, limit, window) triple the bundle used, and drive it from a config file. That makes the limit table diffable during the migration instead of buried in annotations on both sides.

Gotchas and Tips

  • It is effectively abandoned. Last release 2015-02-27, 41 stars, zero dependents. No security patches, no PHP 8 compatibility, no Symfony 4/5/6/7 support. Budget for a fork if you genuinely adopt it.
  • The in-memory store is a trap. Any cache adapter that isn't shared across processes (Array/APCu-per-process, "null" drivers) means each PHP-FPM worker keeps its own counter, so your effective limit is limit × worker_count. The bundle's default/lightweight adapter is very likely to bite here. Always point production at Redis or Memcached, and test with php -S (single process) only for logic, never for limit verification.
  • Non-atomic increments race. Read-then-write counter updates lose hits under concurrency. Redis INCR + EXPIRE (or a short Lua script) is the only safe primitive. Check what the bundle's adapter actually does before trusting the numbers.
  • Fixed windows allow bursts at the boundary. 60 req/min implemented as a fixed window lets a client send 60 at 11:59:59 and 60 at 12:00:00. If that matters, use a sliding window or token bucket — Laravel's Limit::perMinute() is also fixed-window, so this is a "know your semantics" issue rather than a reason to pick either tool.
  • IP resolution behind a proxy. Whatever resolves the key must respect X-Forwarded-For from trusted proxies only, or every client looks like your load balancer (one shared bucket) — or worse, a client spoofs the header and gets infinite buckets. In Laravel, set TrustProxies correctly and prefer $request->ip().
  • 429 isn't the only tool. Return Retry-After so clients back off gracefully instead of hammering. And consider returning 503 if the store itself is down; failing open (no limiting) vs. failing closed (rejecting everything) is a product decision — decide it explicitly.
  • Debugging tactic: log (resolved_key, current_count, limit) at debug level for a single endpoint. Nine out of ten rate-limit bugs are a key resolver returning something unexpected (empty string, ::1, a proxy IP) rather than a broken counter.
  • Extension points worth inspecting (and mirroring in Laravel): the storage adapter interface and the key-resolution hook. Those two seams are the entire design; everything else is Symfony plumbing you don't need to port.
  • Bottom line for a Laravel codebase: read the repo for the adapter abstraction and the header conventions, then use RateLimiter + throttle and be done. Adding this dependency buys you nothing and costs you a Symfony 2 compatibility surface.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings