aporat/laravel-rate-limiter
Add flexible rate limiting to Laravel routes and APIs with aporat/laravel-rate-limiter. Configure request limits, time windows, and custom keys, then protect endpoints via middleware using Laravel’s cache-backed storage. Ideal for throttling login attempts, webhooks, and public APIs.
This is a Redis-backed, multi-window rate limiter for Laravel. v6 is a breaking release, so start with the README's "Upgrading from v5" section before anything else — the removal list (withRateLimitHeaders(), setRequestTag(), getRequestTag(), checkIpAddress(), the redis.* settings, Lumen support) and the header rename will break existing call sites.
Minimum setup:
ext-redis is now optional — use phpredis or predis, whichever your app already ships.connection from config/database.php instead of overriding host/port/database, so point it at an existing connection rather than duplicating credentials.10.0.0.0/8 was deliberately removed. If you sit behind a load balancer, configure trusted proxies before enabling the middleware, or every request will look like the balancer.rate.limiter:60,1 is the quickest smoke test, since middleware parameters are now parsed and validated rather than silently ignored.First real use case: protect a login or token endpoint with a named preset (second=, minute=, hourly=, daily=, key=ip|user|route, name=) and assert the new X-RateLimit-* / Retry-After headers come back on both 200 and 429 responses. That single endpoint exercises windows, keying, headers and the 429 path.
Middleware-driven limiting for the bulk of routes. Combine windows in one directive — rate.limiter:second=10,minute=120,hourly=2000 — and pick a key strategy with key=user, key=ip, or composites like key=ip+route for per-endpoint fairness. Named presets keep long directives out of route files.
Programmatic limits with RateLimit::with() when the limit depends on runtime data (tenant plan, user tier). Builders are immutable in v6, so chaining is safe to do inline and reuse:
$result = RateLimit::with(60, 1)->withUserId($user->id)->withName('api')->attempt($request);
Multi-window atomic checks via RateLimiter::attempt() or PendingLimit::attempt(), which return a LimitResult. Use this when you need to branch on the outcome (e.g. degrade gracefully, queue the job, return custom JSON) instead of throwing. Because all windows are evaluated in one Lua script, a rejected request no longer burns the larger windows — that alone removes a class of "client locked out for an hour" tickets.
Blocking + events for abuse handling. blockedFor() tells you how long a subject is blocked; RateLimitExceeded and IpBlocked let you ship metrics, Slack alerts or audit logs without patching the package. Prefer an event listener over log_errors for anything you actually want to query later.
Client contract: expose X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and Retry-After. These now describe the most restrictive window, so a client backing off on Retry-After behaves correctly. Update any frontend or SDK that parsed the old header names.
Octane / long-running workers: safe by design in v6 — the singleton holds no request state and config is resolved through the Config\Repository contract. Still, don't cache a PendingLimit in a singleton of your own.
Cluster/TLS deployments: all windows for a subject share a hash tag, so a Redis cluster works without cross-slot errors. REDIS_URL, TLS and phpredis/predis are all supported through the Laravel Redis manager.
X-RateLimit-Limit/Remaining/Reset replaced the old names; anything parsing them or asserting in tests will drift without an exception.0 meant to disable limiting no longer works; conditionally skip the middleware instead.10.0.0.0/8 means private ranges are limited by default. Combined with trust-all proxies, that's either a no-op or a shared-bucket outage. Configure TrustProxies deliberately and keep the whitelist loopback-only.on_redis_failure consciously. open fails open (availability over protection); closed returns 503. failure_backoff_seconds throttles error reporting to once per backoff window — tune it so an outage doesn't flood your log pipeline.log_errors is false and, when enabled, writes a single warning with the path only. If you relied on error-level 429 logs with headers and input for debugging, move that to the RateLimitExceeded listener — and note the old behaviour was leaking request data.withUserId('a:b') no longer collides with withUserId('a')->withName('b'). If you pre-computed keys or wrote dashboards parsing raw Redis keys, expect different strings.headers is now true by default, so tests asserting an exact header set may fail.RateLimit call can no longer inherit state from a prior call or from middleware. Code that mutated and reused a builder needs rewriting — usually a simplification, since you can now keep a base builder in a service class.redis.* block is gone. A leftover published config with those keys is dead weight; verify the named connection actually resolves, especially if your default connection is cache rather than redis.EVAL/EVALSHA and SCRIPT LOAD, not just INCR.How can I help you explore Laravel packages today?