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

fusonic/rate-limit-bundle

A Symfony bundle for declarative rate limiting. Apply attributes to controllers and services, configure limits and intervals, and use flexible storage backends to throttle APIs and protect actions with minimal setup and clean Symfony integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is a Symfony bundle, not a Laravel package. There is no service provider, no config/rate_limit.php, no facade. If you composer require fusonic/rate-limit-bundle into a Laravel app, Composer will happily install it and nothing will happen — the bundle only activates inside a Symfony kernel via config/bundles.php. Treat this assessment as "what would I actually get out of this as a Laravel developer," which is: a reference implementation, not a dependency.

What changed in v4.0.0: the release notes are a single line — "Compatibility with PHP 8." That is the first release since 2020-12-11, and it is a major version bump, so treat it as a constraint change, not a feature release. No new limiter algorithms, no Symfony 6/7 work is claimed, no migration guide is offered.

Minimal steps if you are genuinely evaluating it:

  1. composer show fusonic/rate-limit-bundle — check the abandoned flag and repo archive status. A one-line compat release does not mean maintenance resumed; look at commit dates and open-issue response before concluding otherwise.
  2. Diff composer.json between v3.x and v4.0.0 before anything else. In a "PHP 8 compatibility" major, the payload is the constraint bump (php: ^7.x → ^8.0, Symfony floor raises, possibly symfony/* and PHPUnit). That diff is your breaking-change list; the one-line changelog will not tell you.
  3. Read src/DependencyInjection/Configuration.php before the README. The config schema in code is the source of truth; the docs drift, and they drifted for three years.
  4. Skim for the request listener (EventSubscriber on kernel.request or similar) and the limiter/storage class. That pair is the entire package — it is small.
  5. Register the bundle in config/bundles.php, add limits in config/packages/fusonic_rate_limit.yaml, clear the container cache, then hit an endpoint in a loop until you see the 429.

Illustrative config shape (verify keys against Configuration.php):

# config/packages/fusonic_rate_limit.yaml
fusonic_rate_limit:
    default:
        limit: 100
        interval: '1 minute'
    routes:
        app_login:       { limit: 5,  interval: '1 minute' }
        app_password_reset: { limit: 3, interval: '15 minutes' }

First use case: protecting login and password-reset endpoints by route name. That is literally the package's pitch — "rate limiting based on routes" — and it's the one scenario where a route-keyed limiter beats a controller-level one.

The Laravel equivalent you should reach for today (this is what you'd actually ship):

// AppServiceProvider::boot()
RateLimiter::for('login', fn (Request $r) => Limit::perMinute(5)->by($r->ip()));

// routes/api.php
Route::post('/login', LoginController::class)->middleware('throttle:login');

Laravel's throttle middleware and the RateLimiter facade have covered this ground since 5.8. Symfony's own symfony/rate-limiter (5.1+) superseded community bundles like this one — and it has kept pace with Symfony 6/7. The honest reason to read this package is to borrow its keying and windowing choices, or because you've inherited a Symfony app that already depends on it. v4.0.0 removes the "won't even install on PHP 8" objection; it does not create a new reason to adopt it.

Implementation Patterns

v4.0.0 is a compatibility release — the runtime behavior is unchanged. Every pattern below carries over from v3.x verbatim; only the composer constraints moved. Don't re-derive the design from the release note.

Route-name keying, not path keying. The bundle builds its counter key off the matched route, so two routes pointing at the same controller need two config entries, and a request that 404s (no matched route) may be unmetered entirely. If you're mirroring the behavior in Laravel, decide deliberately: key off $request->route()->getName() for precision, or $request->path() for coverage.

Layered limits. The pattern that works: a permissive global default, then tight overrides for auth, password reset, OTP send, invite emails, and webhook receive. Everything else inherits. Keep the overrides in one config block so you can audit them.

Per-client keying and the proxy trap. Nearly every implementation keys on REMOTE_ADDR + route. Behind a load balancer or CDN, REMOTE_ADDR is the balancer, so all users share one bucket and your first busy minute 429s the whole site. In Symfony this means Request::setTrustedProxies(); in Laravel, TrustProxies middleware. Validate this before you trust any load test.

Mirroring into Laravel. If you're porting the idea rather than the code, keep one source of truth for the limit map and express it as named limiters:

// config/rate_limits.php  ->  ['login' => [5, 'minute'], 'webhook' => [120, 'minute']]
foreach (config('rate_limits') as $name => [$max, $per]) {
    RateLimiter::for($name, fn (Request $r) => Limit::per($per)->by($name.'|'.$r->ip()));
}

Then ->middleware('throttle:login'). This gives you the per-route granularity with Laravel's Redis-backed store.

Do not stack two limiters. If you're mid-migration and both the bundle's listener and a Laravel throttle middleware are active on the same route, you double-count and produce 429s that nobody can explain from either config. Pick one layer per route.

Standardize the 429 contract at the edge. Whichever layer emits it, normalize the body and set Retry-After. Clients that only ever see one shape are much easier to debug than "sometimes it's Symfony's HTML error page."

Testing. Feature-test it: issue N requests, assert 200s, then assert 429 on N+1. Freeze time (Carbon::setTestNow, or Symfony's clock) if the limiter uses fixed windows, otherwise a slow CI run can straddle a window boundary and flake. On the Symfony side, run the package's own test suite against the PHP version you actually deploy — the 4.0.0 CI matrix was very likely just 8.0.

Storage. If the bundled storage is in-memory/per-process, it does not survive PHP-FPM workers, and it definitely doesn't survive multiple app nodes. Anything production means Redis-backed counters — check what the bundle actually ships before assuming. The compat release did not modernize storage.

Gotchas and Tips

  • "PHP 8 compatible" ≠ "supported on your PHP 8." A 4.0.0 shipped as a compat bump from a dormant project was almost certainly validated on PHP 8.0 only. On 8.1 you may hit ReturnTypeWillChange deprecations, on 8.2 dynamic-property deprecations, on 8.4 implicit-nullable deprecations — all potentially fatal if your error handler escalates deprecations. Smoke-test on your exact runtime before pinning.
  • Pin ^4.0, and read the major-version bump as a constraint change. The breaking change is that php: ^7.x consumers can no longer install it. If you're still on PHP 7.4 anywhere, stay on the 3.x line (or, better, don't add this dependency).
  • One release does not undo three years of dormancy. Still check for abandoned, archive status, and unanswered issues. No stated Symfony 6/7 support means you may still be blocked at the framework layer even though PHP installs fine.
  • Bundle vs. package semantics. Symfony container config is compiled at build time — editing the YAML without bin/console cache:clear leaves you debugging a config that isn't loaded. Laravel devs are used to config:cache being optional; here it isn't.
  • The README lies eventually. Configuration.php is authoritative for key names, defaults, and whether limits are integers or interval strings ('1 minute' vs 60 vs a \DateInterval). Read the code.
  • Route-name matching silently no-ops. A typo in a route name means the override never applies and the default catches the traffic — you get a working-but-wrong limiter. Assert the route name exists in a boot-time check or a test.
  • Fixed windows allow 2× burst. A client can spend the full quota at 0:59 and again at 1:00. For login and OTP endpoints prefer a sliding window or token bucket; for bulk API reads a fixed window is fine and cheaper.
  • Fail open on storage errors. If Redis blips and the limiter throws, don't propagate a 429 — that turns a cache incident into an outage. Catch, log, allow the request. Conversely, don't fail open on the auth endpoints without alerting.
  • 404s may be unmetered (no matched route), which makes route-name limiting a poor defense against enumeration/scraping. Put a path-prefix or IP-only limiter at the proxy for that.
  • Extension point in Symfony is service decoration — alias the storage/limiter service to your own implementation to swap Redis in or change the key strategy. In Laravel the equivalent hooks are RateLimiter::for() plus a custom cache store; there's no reason to route through a bundle for this.
  • Exit cost is still near zero. The package is a small request listener, and the 4.0.0 release added zero functionality. Reimplementing it as ~40 lines of Laravel middleware is cheaper than carrying a revived-but-untested dependency and its transitive constraints through future framework upgrades. If you like the design, copy the design.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console