Throttling & Rate Limits#
Protect endpoints from abuse and smooth load by applying per-route or grouped throttles. Webrick’s throttle middleware returns 429 Too Many Requests with helpful headers so clients know when to retry.
What throttling does#
Enforces a burst + window limit (e.g., “10 requests per minute”).
Emits informative headers:
Retry-After(seconds until safe to retry)X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-ResetOptionally IETF
RateLimit-*headers if enabled
Adds server timing hints for observability.
Apply conservative limits on public routes; loosen for authenticated/internal traffic.
Enable the middleware#
Add it to pre-global or use per-route options.
$preGlobal = [
// ...
\Infocyph\Webrick\Middleware\ThrottleMiddleware::class,
// ...
];
Per-route throttles#
Use a concise throttle:<max>,<perSeconds> syntax in route options.
// 5 requests per minute
Route::get('/otp', fn()=> 'ok', [
'middleware' => ['throttle:5,60'],
]);
// Tighter on login
Route::post('/login', fn()=> 'ok', [
'middleware' => ['throttle:10,300'], // 10 per 5 minutes
]);
Group-level throttles#
Apply one limit to an entire module:
Route::group(
prefix:'/api',
namePrefix:'api.',
middleware:['throttle:120,60'], // 120/minute across all API routes
callback:function ($api) {
$api->get('/status', fn()=> ['ok'=>true], 'status');
}
);
Routes can add their own per-route throttle on top (becomes stricter).
Keying & fairness#
Typical keying strategies (implementation-dependent):
Anonymous: client IP (be careful behind proxies; trust only known forwarders)
Authenticated: user ID or API token
Composite:
userID + routeNamefor per-endpoint fairness
For multi-tenant apps, include tenant/org ID in the key to avoid noisy neighbors.
Client-visible headers#
A successful request includes remaining budget:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1738961123
A throttled request:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Remaining: 0
Optionally emit the standard RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset as well.
Handling bursts & backoff#
Favor token bucket or leaky bucket semantics to allow short bursts.
Clients should:
Respect
Retry-AfterImplement exponential backoff (e.g., 1s, 2s, 4s, … up to a ceiling)
Avoid synchronizing retries across many clients
Testing#
# Hit 6 times quickly against throttle:5,60
for i in {1..6}; do curl -i http://127.0.0.1:8000/otp; done
Expect first 5 = 200, 6th = 429 with Retry-After.
Common patterns#
Stricter unauthenticated, looser authenticated#
Route::group(middleware:['throttle:60,60'], callback:function () {
Route::post('/login', fn()=> 'ok', ['middleware'=>['throttle:10,300']]);
});
Route::group(middleware:['auth','throttle:600,60'], callback:function () {
Route::get('/me', fn()=> ['ok'=>true]);
});
Endpoint-specific “expensive” operations#
Route::post('/export', fn()=> 'queued', ['middleware'=>['auth','throttle:3,3600']]);
Operational tips#
Observability: surface throttle counters in metrics; enable
Server-Timinghints for tracing.Edge/CDN: if terminating at an edge, perform coarse throttles there and fine-grained app throttles in Webrick.
Abuse spikes: pair with IP reputation, CAPTCHA, or proof-of-work for ultra-hot endpoints (login, OTP, password reset).
Safe-lists: allow internal monitors/healthchecks via bypass key or CIDR allowlist (carefully).
Checklist#
Add global throttle middleware
Set sensible defaults per module (
/api,/auth)Use per-route throttles for sensitive/expensive endpoints
Pick correct key (IP vs user vs token vs tenant)
Verify headers appear; clients should obey
Retry-AfterMonitor 429 rates and tune limits accordingly