Search by

hanzala-afzal / laravel-dynamic-throttle

hanzala-afzal-dev

Runtime-managed, identity-aware rate limiting for Laravel: change limits without a deploy, per user or token, and dry-run them first.

v0.2.0 2026-11-04 18:46 UTC

README

The Dynamic Throttle logo: a red D whose curve is a gauge, its needle near the red end of the scale.

Dynamic Throttle for Laravel

Change your app's rate limits while it runs, and dry-run them first.

Latest version on Packagist Total downloads PHP 8.3, 8.4 and 8.5 Laravel 12 and 13 MIT license

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.

A limit changing while the app runs. On the left, a terminal loops curl against /api/orders, and every response is 200. On the right, the dashboard edits the Orders API rule: its limit goes from 100 to 2 requests a minute, and it is saved. Within two seconds, the terminal's responses turn to 429 Too Many Requests, with no deploy.

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 file or apc in 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:

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.