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

Ratelimit Bundle Laravel Package

noxlogic/ratelimit-bundle

RateLimitBundle adds flexible, configurable rate limiting to Symfony applications. Protect APIs and routes from abuse, define custom limits and strategies, and integrate quickly with minimal boilerplate. Ideal for throttling requests and controlling traffic.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check every Laravel dev needs: noxlogic/ratelimit-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no facade, and no config/ratelimit.php. It hooks kernel.controller and reads @RateLimit annotations/attributes. Composer will install it into a Laravel app, but nothing will wire up — you'd be hand-registering a Symfony DI extension inside Laravel's container. Don't. If you're in Laravel, the equivalent is Illuminate\Cache\RateLimiter + the throttle middleware, and you already have it.

So this assessment is written for the realistic case: you're on a polyglot team, or maintaining/porting a Symfony service alongside your Laravel ones, and you need to evaluate this bundle on its merits or mirror its behaviour in Laravel.

Minimal setup in an actual Symfony app:

composer require noxlogic/ratelimit-bundle
// config/bundles.php
Noxlogic\RateLimitBundle\NoxlogicRateLimitBundle::class => ['all' => true],
# config/packages/noxlogic_rate_limit.yaml
noxlogic_rate_limit:
    enabled: true
    storage_engine: redis          # redis | memcache | in-memory | doctrine
    rate_response_code: 429
    display_headers: true
    limiters:
        anonymous:     { limit: 60,   period: 60 }
        authenticated: { limit: 3000, period: 3600 }
    paths:
        '^/api/login':
            limits: { anonymous: 5 }
            methods: [POST]
        '^/api':
            limits: { anonymous: 60, authenticated: 3000 }

Where to look first, in this order:

File Why
DependencyInjection/Configuration.php The real, authoritative config tree — the README lags behind
Annotation/RateLimit.php (also attribute form) The per-action API: limit, period, key, methods
EventListener/*Listener.php The kernel.controller hook — this is where enforcement actually happens
Service/RateLimiter.php + RateLimiterInterface hit() / check() / reset() — your integration seam
Storage/ Adapters per backend; the key format lives here

First use case: throw it in front of POST /login and POST /password-reset. Those are the two endpoints where a rate limit is a security control rather than a politeness feature, and they're the fastest way to confirm the storage engine is actually shared across PHP-FPM workers (if it's in-memory, it isn't — see Gotchas).

Laravel mapping, for when you translate requirements between the two codebases:

This bundle Laravel
paths: regex block Route::middleware('throttle:60,1')
Named limiters: tiers RateLimiter::for('api', fn ($r) => ...)
RateLimit(limit=, period=) attribute Per-route throttle: or a custom limiter
RateLimiterInterface::hit() RateLimiter::hit($key, $decay)
X-RateLimit-* headers throttle middleware headers (Laravel 10+)

Implementation Patterns

Path regex config is the primary mechanism, annotations are the exception handler. Use paths: for blanket rules on URL prefixes (API surface, auth endpoints), and reserve the @RateLimit attribute for one-off expensive actions — report generation, search, SMS send. Mixing both on the same route is allowed; the most restrictive limit wins, which is usually what you want but makes debugging harder.

Tiered limiters keyed by identity. Define anonymous and authenticated limiters and apply them by path; the bundle picks the matching limiter. The default key derives from the client IP, which is fine for anonymous traffic and wrong for authenticated traffic — a café NAT shouldn't share one bucket across fifty users. For authenticated endpoints, set the annotation/limiter key to a stable user or tenant identifier:

#[RateLimit(limit: 100, period: 3600, key: 'user')]
public function exportAction(): Response { /* ... */ }

(Verify the key-resolution signature against Annotation/RateLimit.php in your installed version — it changed between the annotation and attribute eras.)

Pick Redis, and only Redis, above one server. in-memory is per-process and therefore per-request under PHP-FPM; doctrine needs a shared cache pool. Memcached works but eviction under memory pressure silently resets limits — an attacker can't exploit it, but your metrics will lie. Redis gives you a shared counter plus inspectable keys, which matters enormously when someone asks "why was I blocked at 3am".

Own the 429 response. The bundle sets the status code and X-RateLimit-* headers, but the body is generic. Register a small kernel exception listener for RateLimitExceededException and return your API's standard error envelope plus a Retry-After. This is ~15 lines and is the difference between "professional API" and "Symfony default error page leaking a stack trace".

Cost-based limits need a different shape. The bundle is a counter, not a budget — one request costs one. If an endpoint's cost varies (bulk endpoint with ?ids= up to 500), rate-limit on a computed weight in your controller via RateLimiterInterface::hit($key) in a loop, or move the logic into a service and leave the bundle to cover the cheap routes.

Testing. Set storage_engine: in-memory in config/packages/test/ and reset between tests via the limiter's reset(). Never let functional tests touch the shared Redis you use in dev — you will flake your own test suite the first time a test suite runs the 61st request.

Sharing counters with a Laravel app. Tempting, and mostly a trap: the bundle's Redis key format (prefix + key) won't match Laravel's RateLimiter. Two apps hitting the same endpoint will each enforce their own budget. If you genuinely need a shared budget, put the limiter in one service (nginx/Envoy/API gateway) rather than trying to align key formats across frameworks.

Gotchas and Tips

  • Reverse proxies are the #1 production bug. Behind an ALB/Cloudflare, every request appears to come from the load balancer's IP, so the entire internet shares one bucket — or, if the LB IP is whitelisted, nobody is limited at all. You must configure framework.trusted_proxies / trusted_headers correctly. Validate before launch by echoing the resolved client IP.
  • Anchor your path regexes. ^/api matches; /api will also match /foo/api/bar and surprise you. Also check ordering — a broad ^/ catch-all listed first can shadow everything below it.
  • Attribute vs. docblock annotations. Old installs use @RateLimit(...) docblocks and require doctrine/annotations; newer ones use PHP 8 attributes. A silent no-op rate limit is almost always an annotation the reader never parsed. Verify by asserting a 429 in a functional test, not by reading config.
  • In-memory storage in dev masks all bugs. Limits appear to work (each request counter starts at zero, so nothing ever trips), then production behaves differently. Run redis locally.
  • Headers only appear on matched paths. If display_headers: true but the route doesn't match a paths: entry or attribute, you get no X-RateLimit-* at all. Clients that pre-emptively back off on Remaining will be blind on unconfigured routes.
  • Fixed windows allow a 2x burst at the boundary. 60 requests at 12:59:59 followed by 60 at 13:00:00 is legal within "60/minute". Accept it, or implement a sliding window on top if you're defending against credentialed brute force.
  • Redis outages are not gracefully degraded by default. Decide explicitly: fail open (availability) or fail closed (protection). Wrap the storage adapter or decorate RateLimiterInterface — this is the cleanest extension point the bundle offers.
  • Custom storage / custom behaviour = decoration, not config. The storage_engine option is an enum-like switch inside the DI extension, not an arbitrary service ID in older versions. To add a backend, implement the storage interface and re-wire via a compiler pass, or decorate the limiter service. Check DependencyInjection/ before assuming a config knob exists.
  • Bus factor. One maintainer, zero dependents, latest release 2025-09-08. Pin the exact version in composer.json, read the CHANGELOG on every bump, and keep your own functional test asserting the 429 — if upstream stalls, that test tells you immediately and the swap to a hand-rolled listener is a day's work.
  • Don't rate-limit healthchecks. Add ^/health, ^/_profiler, and asset paths to an explicit bypass, or your monitoring will page you for rate limiting the monitor.
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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings