hanzala-afzal / laravel-dynamic-throttle
Runtime-managed, identity-aware rate limiting for Laravel: change limits without a deploy, per user or token, and dry-run them first.
Package info
github.com/hanzala-afzal-dev/laravel-dynamic-throttle
pkg:composer/hanzala-afzal/laravel-dynamic-throttle
Requires
- php: ^8.3
- laravel/framework: ^12.69.0 || ^13.30.0
Requires (Dev)
- larastan/larastan: ^3.13
- laravel/pint: ^1.24
- orchestra/testbench: ^10.6 || ^11.0
- pestphp/pest: ^4.1 || ^5.0
- pestphp/pest-plugin-laravel: ^4.0 || ^5.0
- phpbench/phpbench: ^1.7
- predis/predis: ^2.3 || ^3.0
- rector/rector: ^2.1
Suggests
- ext-apcu: Enables the per-server L1 rule cache on PHP-FPM (see docs/adr/0007).
- ext-redis: Needed by the redis driver when you use the phpredis client.
- predis/predis: Needed by the redis driver when you use the predis client instead of ext-redis.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 05:56:13 UTC
README
Dynamic Throttle for Laravel
Change your app's rate limits while it runs, and dry-run them first.
Dynamic Throttle for Laravel lets you change your app's rate limits while it runs: per IP, user, API token, header or tenant, with a dry-run mode, exemptions, emergency switches and a dashboard.
Why
Laravel's throttle middleware works for limits you know up front. In production, you learn the right limits later, and in a hurry: a partner's integration starts getting 429s, a bot hammers /login at 2 a.m., or you want a stricter limit without knowing who it would block. With throttle, each fix is a code change and a deploy, and you still can't see who is blocked.
Dynamic Throttle makes limits data. Rules live in your database, you change them from a dashboard, Artisan or code, and every server applies the change within seconds.
| You want to | Laravel's throttle |
Dynamic Throttle |
|---|---|---|
| Change a limit | Edit code and deploy | Edit the rule; servers apply it within 2 seconds |
| Try a limit before it blocks anyone | Not possible | Dry-run: the rule counts, never blocks, and reports who it would block |
| See who is blocked | Add your own logging | A live feed, and each rule's top offenders |
| Let a partner or a user through | A code change | An exemption, with an end date |
| Lift every limit during an incident | A deploy | dynamic-throttle:bypass on --for=30m, which ends by itself |
| Limit per tenant or plan | A custom limiter closure | A custom scope and one rule |
| Know why a request was blocked | Read the code | The Explain page, or dynamic-throttle:explain |
| Keep serving when Redis is down | An exception per request | Requests go through, or get a 503 on the rules you choose |
Requests never read the rules from the database: they are compiled into your cache. On local Redis, the middleware adds about 0.8 ms per request at p50 with APCu, against 1.0 ms for Laravel's throttle and 0.6 ms for its throttleWithRedis() (performance). It is not a firewall or bot detection, and it does not replace edge rate limiting such as Cloudflare or Nginx. Use it for limits that need to know who the client is.
The dashboard
It ships with the package at /dynamic-throttle, behind your gates, with nothing to build: Overview, Rules, Exemptions, Activity (a live feed of blocks), Explain, Audit log and Settings, in light and dark themes.
Requirements
- PHP 8.3+, and Laravel 12.69+ or 13.30+.
- A counter store: Redis 6.2+ or Valkey 7.2+ is best, and the live feed and offenders need one. Memcached and the database store work too, but not
fileorapcin production. - On PHP-FPM, the APCu extension, so a server's workers share the rules.
Installation
1. Install:
composer require hanzala-afzal/laravel-dynamic-throttle
2. Create the tables, and compile the rules once. After this, saving a rule compiles them, and so does the scheduler.
php artisan vendor:publish --tag=dynamic-throttle-migrations php artisan migrate php artisan dynamic-throttle:compile
3. Attach the middleware to the route groups rules may limit, in bootstrap/app.php. Routes without it are never limited.
->withMiddleware(function (Middleware $middleware) { $middleware->appendToGroup('web', 'dynamic.throttle'); // pages and forms, such as login and sign-up $middleware->appendToGroup('api', 'dynamic.throttle'); // routes/api.php })
A new Laravel app has no API routes until you run php artisan install:api. The dashboard's own routes are never limited.
4. Before you deploy, say who may use the dashboard, in a service provider's boot(). On your machine, this can wait: in the local environment, anyone can open the dashboard. Once you define the gates, they decide there too, so sign in to open it. Until you do, php artisan dynamic-throttle:health warns everywhere but local. An app without a user login can use one-time links instead.
use App\Models\User; use Illuminate\Support\Facades\Gate; Gate::define('viewDynamicThrottle', fn (User $user) => in_array($user->email, ['you@example.com'], true)); Gate::define('manageDynamicThrottle', fn (User $user) => in_array($user->email, ['you@example.com'], true));
5. Run Laravel's scheduler. It recompiles the rules every five minutes and stores the traffic numbers every minute.
Then check your setup with php artisan dynamic-throttle:health. With Redis, it warns until the counters have a connection of their own and every connection requests use has short timeouts: timeouts shows both.
Usage
Add a rule in the dashboard under Rules → New rule, from a template such as "Login" or "API per user". New rules start in dry-run: they count requests and show who they would block, but block no one until you promote them.
Or from code:
use Afzal\DynamicThrottle\Models\Rule; Rule::create([ 'name' => 'API per user', 'pattern' => 'api/*', 'scope' => 'user', // ip, user, api_token, header, global or custom 'max_attempts' => 60, 'window_seconds' => 60, ]);
Each user now gets 60 requests a minute under /api. The 61st gets Laravel's own 429, with Retry-After and X-RateLimit-* headers.
| Command | What it does |
|---|---|
dynamic-throttle:health |
Looks for setup mistakes. --strict fails on a warning too |
dynamic-throttle:explain GET /api/orders --user=42 |
Shows why a request would be allowed or limited, without counting it |
dynamic-throttle:status |
Shows what the servers apply now, and until when |
dynamic-throttle:bypass on --for=30m |
Lifts every limit until the end time |
dynamic-throttle:scale 2 --for=2h |
Multiplies every limit: 2 doubles them, 0.5 halves them |
dynamic-throttle:disable-rule 12, enable-rule 12 |
Switches one rule off or back on |
dynamic-throttle:list |
Lists every rule with its id |
dynamic-throttle:compile |
Recompiles the rules now |
Documentation
The guide has the details, also on the documentation site, with search:
- Rules: every attribute, examples, your own scope, and exemptions
- An endpoint that costs money, such as one that calls an AI model: a limit per user or per plan, tried in dry-run first
- Dry-run rules, events and testing your app with
DynamicThrottle::fake() - Configuration, attaching the middleware, and storage and runtimes
- The dashboard, health checks and during an incident
- The runbook: Redis is down, the manifest was evicted, a bad rule blocks
/api, or an emergency change was left on
Feedback
What helps most:
- Did it install cleanly on your Laravel and PHP versions, cache store and runtime (PHP-FPM, Octane, Vapor)?
- Did a rule do something you did not expect? Paste the output of
php artisan dynamic-throttle:explain. - Was anything in the dashboard or the docs unclear?
Report bugs in issues, and bring questions and ideas to discussions. Report security issues privately, as SECURITY.md explains, never in a public issue.
License
MIT, © 2026 Hanzala Afzal. See LICENSE and the changelog.
An independent package, not affiliated with or endorsed by Laravel. "Laravel" is a trademark of its respective owner.
