Rate Limiting & Circuit Breaker
Retail-API implements rate limiting at multiple levels to protect against abuse and ensure fair resource usage. Additionally, a circuit breaker pattern protects against cascading failures at locality level.
Components
Section titled “Components”| Component | Location | Purpose |
|---|---|---|
| ThrottleRequests | Laravel built-in | Request rate limiting |
| RouteServiceProvider | app/Providers/ | Defines api and webhooks rate limiters |
| RateLimiterServiceProvider | app/Providers/ | Defines security-related rate limiters |
| RequestCircuitBreaker | app/Http/Middleware/ | Locality-based circuit breaker |
| CircuitBreakerHelper | app/Http/Controllers/Support/ | Circuit breaker state management |
1. Rate Limiter Configuration
Section titled “1. Rate Limiter Configuration”RouteServiceProvider
Section titled “RouteServiceProvider”File: app/Providers/RouteServiceProvider.php
Defines the main rate limiters for API groups.
private function configureRateLimiting(): void{ RateLimiter::for('api', function (Request $request): Limit { return Limit::perMinute(600)->by((string) ($request->user()->id ?? $request->ip() ?? '127.0.0.1')); });
RateLimiter::for('webhooks', function (Request $request): Limit { return Limit::perMinute(600)->by($request->ip() ?? '127.0.0.1'); });}RateLimiterServiceProvider
Section titled “RateLimiterServiceProvider”File: app/Providers/RateLimiterServiceProvider.php
Defines security-related rate limiters.
final class RateLimiterServiceProvider extends ServiceProvider{ public function boot(): void { // OTP verification RateLimiter::for('otp-verify', function (Request $request): Limit { return Limit::perHour(10)->by($request->ip()); });
// OTP resend (multiple limits) RateLimiter::for('otp-resend', function (Request $request): array { return [ Limit::perHour(10)->by($request->ip()), Limit::perMinute(1)->by($request->ip()), ]; });
// WebAuthn authentication RateLimiter::for('webauthn-authentication', function (Request $request): array { $max_attempts = config('webauthn.rate_limit.authentication_max_attempts');
return [ Limit::perHour($max_attempts['per_hour'])->by($request->ip()), Limit::perMinute($max_attempts['per_min'])->by($request->ip()), ]; });
// WebAuthn registration RateLimiter::for('webauthn-registration', function (Request $request): array { $max_attempts = config('webauthn.rate_limit.registration_max_attempts');
return [ Limit::perHour($max_attempts['per_hour'])->by($request->user()?->id), Limit::perMinute($max_attempts['per_min'])->by($request->user()?->id), ]; }); }}Rate Limiter Summary
Section titled “Rate Limiter Summary”| Limiter | Limits | Key By | Use Case |
|---|---|---|---|
api | 600/min | User ID or IP (fallback: 127.0.0.1) | Default API group |
webhooks | 600/min | IP (fallback: 127.0.0.1) | Webhook endpoints |
otp-verify | 10/hour | IP | OTP verification |
otp-resend | 10/hour + 1/min | IP | OTP resend (anti-spam) |
webauthn-authentication | configurable | IP | WebAuthn login |
webauthn-registration | configurable | User ID (nullable) | WebAuthn credential creation |
2. ThrottleRequests Middleware
Section titled “2. ThrottleRequests Middleware”File: Laravel’s Illuminate\Routing\Middleware\ThrottleRequests
Middleware Group Usage
Section titled “Middleware Group Usage”protected $middlewareGroups = [ 'api' => [ ThrottleRequests::class . ':api', // Use 'api' rate limiter // ... ], 'webhooks' => [ ThrottleRequests::class . ':webhooks', // ... ], 'internal' => [ ThrottleRequests::class . ':120,1', // 120 requests per minute (inline) // ... ],];Route-Level Throttling
Section titled “Route-Level Throttling”// Use named limiterRoute::post('otp/verify', [OtpController::class, 'verify']) ->middleware('throttle:otp-verify');
Route::post('otp/resend', [OtpController::class, 'resend']) ->middleware('throttle:otp-resend');
// WebAuthn with nested middlewareRoute::group(['middleware' => 'prevent_privileged_impersonation'], function () { Route::post('webauthn/registration/initialize', ...) ->middleware('throttle:webauthn-registration');});
// Inline definition (simple cases)Route::post('simple-action', [SimpleController::class, 'store']) ->middleware('throttle:60,1'); // 60 requests per minuteResponse Headers
Section titled “Response Headers”When rate limiting is active, responses include:
X-RateLimit-Limit: 600X-RateLimit-Remaining: 599X-RateLimit-Reset: 1699999999Rate Limit Exceeded Response
Section titled “Rate Limit Exceeded Response”{ "message": "Too Many Attempts."}HTTP Status: 429 Too Many Requests
Headers:
Retry-After: 60X-RateLimit-Limit: 600X-RateLimit-Remaining: 0X-RateLimit-Reset: 16999999993. Multiple Rate Limits
Section titled “3. Multiple Rate Limits”Some actions require multiple limits (burst + sustained):
RateLimiter::for('otp-resend', function (Request $request): array { return [ // Sustained limit: 10 per hour Limit::perHour(10)->by($request->ip()),
// Burst limit: 1 per minute Limit::perMinute(1)->by($request->ip()), ];});This prevents:
- Spam attacks (burst limit)
- Long-term abuse (sustained limit)
How Multiple Limits Work
Section titled “How Multiple Limits Work”Both limits are checked independently. If either limit is exceeded, the request is rejected.
Request 1: OK (hour: 1/10, minute: 1/1)Request 2 (within 1 min): REJECTED (minute: exceeded)Request 2 (after 1 min): OK (hour: 2/10, minute: 1/1)...Request 11 (within 1 hour): REJECTED (hour: exceeded)4. Circuit Breaker
Section titled “4. Circuit Breaker”File: app/Http/Middleware/RequestCircuitBreaker.php
Protects the system from cascading failures when a locality (household/advisor/organization) is causing problems.
Implementation
Section titled “Implementation”final class RequestCircuitBreaker{ public function handle(Request $request, Closure $next): Response { $method = $request->method();
$circuit_status = LocalityManager::switchIgnoreLicenseStatus( IGNORE_LICENSE_STATUS, fn (): array => CircuitBreakerHelper::getLocalityCircuitStatus(LocalityManager::locality(), $method), );
if ($circuit_status['is_broken']) { // Create error message with reason $message = 'Service is temporarily unavailable';
if ($circuit_status['reason'] !== null) { $message .= ': ' . $circuit_status['reason']; }
// Let the global error handler deal with the response formatting throw new HttpException(429, $message); }
return $next($request); }}Key Behavior
Section titled “Key Behavior”- Uses
LocalityManagerto get current locality context - Checks circuit status for the specific HTTP method being requested
- Error message: “Service is temporarily unavailable: {reason}”
- Throws
HttpException(429)when circuit is broken
5. CircuitBreakerHelper
Section titled “5. CircuitBreakerHelper”File: app/Http/Controllers/Support/CircuitBreakerHelper.php
Manages circuit breaker state in Redis cache.
Implementation
Section titled “Implementation”final class CircuitBreakerHelper{ public const string LEVEL_HOUSEHOLD = 'household'; public const string LEVEL_ADVISOR = 'advisor'; public const string LEVEL_ORGANIZATION = 'organization';
public const array HTTP_METHODS = [ Request::METHOD_GET, Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_DELETE, ];
private const int DEFAULT_CIRCUIT_BREAK_TTL = 300; // 5 minutes private const string CIRCUIT_BREAKER_CACHE_KEY_PREFIX = 'circuit_breaker:';
public static function breakCircuitGivenResourceAndMethod( string $resource_type, int $resource_id, string $reason, array|null $methods = null, int|null $ttl = null, ): void { $cache_key = self::buildCacheKey($resource_type, $resource_id); $methods ??= self::HTTP_METHODS;
Cache::store('redis')->put( $cache_key, [ 'reason' => $reason, 'methods' => $methods, ], $ttl ?? self::DEFAULT_CIRCUIT_BREAK_TTL, ); }
public static function getLocalityCircuitStatus(Locality $locality, string|null $method = null): array { // Checks hierarchy based on locality type: // HouseholdLocality: household → advisor → organization // AdvisorLocality: advisor → organization // OrganizationLocality: organization
// Returns: ['is_broken' => bool, 'reason' => string|null, 'level' => string|null, 'id' => int|null, 'methods' => array] }}Circuit Levels
Section titled “Circuit Levels”The circuit breaker checks a hierarchy based on locality type:
| Locality Type | Check Order |
|---|---|
HouseholdLocality | household → advisor → organization |
AdvisorLocality | advisor → organization |
OrganizationLocality | organization |
Cache Key Format
Section titled “Cache Key Format”circuit_breaker:household:123circuit_breaker:advisor:456circuit_breaker:organization:789Cache Value Format
Section titled “Cache Value Format”[ 'reason' => 'Calculation overload', 'methods' => ['GET', 'POST', 'PUT', 'DELETE'], // Which methods are blocked]Default TTL
Section titled “Default TTL”- 300 seconds (5 minutes)
Breaking a Circuit
Section titled “Breaking a Circuit”// Block all methods for a householdCircuitBreakerHelper::breakCircuitGivenResourceAndMethod( resource_type: CircuitBreakerHelper::LEVEL_HOUSEHOLD, resource_id: $household->id, reason: 'Calculation overload',);
// Block only POST and PUT for an advisorCircuitBreakerHelper::breakCircuitGivenResourceAndMethod( resource_type: CircuitBreakerHelper::LEVEL_ADVISOR, resource_id: $advisor->id, reason: 'Too many write operations', methods: ['POST', 'PUT'], ttl: 120, // 2 minutes);Resetting a Circuit
Section titled “Resetting a Circuit”// Reset all methodsCircuitBreakerHelper::resetCircuitGivenResourceAndMethod( resource_level: CircuitBreakerHelper::LEVEL_HOUSEHOLD, resource_id: $household->id,);
// Reset specific methods onlyCircuitBreakerHelper::resetCircuitGivenResourceAndMethod( resource_level: CircuitBreakerHelper::LEVEL_ADVISOR, resource_id: $advisor->id, methods: ['POST'],);6. Circuit Breaker Response
Section titled “6. Circuit Breaker Response”{ "message": "Service is temporarily unavailable: Calculation overload"}HTTP Status: 429 Too Many Requests
7. Rate Limiting Best Practices
Section titled “7. Rate Limiting Best Practices”Choosing Key Strategy
Section titled “Choosing Key Strategy”// Per-user (authenticated endpoints)Limit::perMinute(100)->by($request->user()->id);
// Per-IP (public endpoints)Limit::perMinute(30)->by($request->ip());
// Composite key (granular control)Limit::perMinute(50)->by($request->user()->id . '|' . $request->ip());
// Per-resource (specific resource protection)Limit::perMinute(10)->by( $request->user()->id . '|' . $request->route('household'));Configurable Limits
Section titled “Configurable Limits”// Read from config for easy adjustmentRateLimiter::for('webauthn-authentication', function (Request $request): array { $config = config('webauthn.rate_limit.authentication_max_attempts');
return [ Limit::perHour($config['per_hour'])->by($request->ip()), Limit::perMinute($config['per_min'])->by($request->ip()), ];});Config file:
return [ 'rate_limit' => [ 'authentication_max_attempts' => [ 'per_hour' => env('WEBAUTHN_AUTH_MAX_PER_HOUR', 40), 'per_min' => env('WEBAUTHN_AUTH_MAX_PER_MIN', 20), ], 'registration_max_attempts' => [ 'per_hour' => env('WEBAUTHN_REG_MAX_PER_HOUR', 20), 'per_min' => env('WEBAUTHN_REG_MAX_PER_MIN', 10), ], ],];8. Middleware Groups Summary
Section titled “8. Middleware Groups Summary”| Group | Rate Limit | Circuit Breaker |
|---|---|---|
web | None (session-based) | Yes |
api | 600/min | No |
webhooks | 600/min | No |
internal | 120/min | No |
Monitoring & Debugging
Section titled “Monitoring & Debugging”Check Rate Limit Status
Section titled “Check Rate Limit Status”// In controller or service$limiter = app(RateLimiter::class);$key = 'otp-verify:' . request()->ip();
$attempts = $limiter->attempts($key);$remaining = $limiter->remaining($key, 10);$availableIn = $limiter->availableIn($key);Redis Keys
Section titled “Redis Keys”Rate limits are stored in Redis with keys like:
laravel_cache:throttle:otp-verify:192.168.1.1laravel_cache:throttle:api:user_123Circuit Breaker Keys
Section titled “Circuit Breaker Keys”circuit_breaker:household:123circuit_breaker:advisor:456circuit_breaker:organization:789