AddRateLimitHeadersMiddleware: Rate Limit Headers
The throttle middleware stops requests when the limit is exceeded, but clients shouldn't have to guess how close they are to the limit. That's where AddRateLimitHeadersMiddleware comes in, providing complete transparency through HTTP headers and allowing smart clients to self-regulate before hitting the limit.
After the response is generated, this middleware reads the rate limiter's state and adds four critical headers:
X-RateLimit-Limit: The maximum number of requests allowed in the window (e.g., 60)X-RateLimit-Remaining: How many requests are left in the current window (e.g., 42 after you've made 18 requests)X-RateLimit-Reset: Unix timestamp when the quota resets (e.g., 1708982564)Retry-After: Only set when the limit is exceeded; tells the client how many seconds to wait before retrying
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$token = $request->bearerToken() ?? $request->ip();
$key = 'api-token:'.$token;
$maxAttempts = 60;
$remainingAttempts = $this->limiter->remaining($key, $maxAttempts);
$retryAfter = $this->limiter->availableIn($key);
$response->headers->set('X-RateLimit-Limit', (string) $maxAttempts);
$response->headers->set('X-RateLimit-Remaining', (string) max(0, $remainingAttempts));
if ($remainingAttempts <= 0) {
$response->headers->set('X-RateLimit-Reset', (string) (time() + $retryAfter));
$response->headers->set('Retry-After', (string) $retryAfter);
}
return $response;
}
The key insight: X-RateLimit-Remaining is available on every response, not just rejections. This means clients can monitor their quota continuously, even when they're nowhere near the limit.
Reactive clients (poorly designed):
- Send requests normally
- Get a 429 response
- Back off for Retry-After seconds
- Retry
This is wasteful, they burn requests until they hit the wall.
Proactive clients (well-designed):
- Send a request
- Read
X-RateLimit-Remaining: 5from the response headers - Slow down their request rate
- Back off before hitting 0
They never even trigger the rate limit because they self-regulate. Your API is happier because it isn't rejecting requests. The client is happier because they get consistent service.
Rate limiting is tricky with concurrent clients. Imagine a client makes 30 concurrent requests. The first request sees 60 - 30 = 30 remaining. But by the time all 30 hit your API, the counter has already decremented. Clients need to understand that X-RateLimit-Remaining is a snapshot, not a guarantee. It's accurate at that moment, but not in the future.
This is why smart clients use X-RateLimit-Remaining as a guide, not a law. They might back off at 10 remaining instead of 0, giving themselves a safety buffer for concurrent activity.
Many HTTP client libraries now read these headers automatically and implement backoff strategies. The Python requests-throttler library, for example, watches X-RateLimit-Remaining and automatically pauses requests. JavaScript's axios with rate limit middleware does the same. By exposing these headers, you enable sophisticated client behavior without having to build it yourself.