Skip to main content
Laravel, shipping fast.

Chapter 4

The Request Pipeline

Julian Beaujardin

Authentication answers one question: who is this. Everything else a request meets on its way in answers a different one — how much of your API may it use, in what language, at what size, from which origin, and what record survives it afterwards.

Those concerns share a property that makes them worth a chapter of their own: not one of them belongs to any single endpoint. They are decided once, in the order the middleware runs, and then they are true everywhere. Get the order wrong and a rate limit protects nothing because the work already happened; forget one and a single endpoint quietly behaves differently from the other ninety-nine.

Rate Limiting: throttle:api-token

Rate limiting is your API's defense against abuse and resource exhaustion. Without it, a single misconfigured or malicious client can hammer your backend and degrade performance for everyone else. The throttle:api-token middleware is Laravel's built-in rate limiting system, and it's configured specifically for this API to limit requests per Bearer token.

The limiter is registered once, in the shared package's service provider, so every service in the fleet throttles identically without copying the rule:

RateLimiter::for('api-token', function (Request $request) {
    /** @var int $limit */
    $limit = config('webplo.rate_limit', 60);

    return Limit::perMinute($limit)->by($request->bearerToken() ?: $request->ip());
});

This does three things: it defines a limiter named 'api-token', takes the ceiling from config rather than hardcoding it, and keys the limit to the Bearer token when present, falling back to the client's IP when there is none.

Note what the fallback does and does not do. A request with no token is limited by IP, not rejected — rejecting it belongs to the authentication middleware, which runs first and never lets an unauthenticated request reach the limiter at all. Giving the limiter two jobs is how you end up with a limiter that quietly authorises.

The crucial part is by($request->bearerToken() ?: $request->ip()). Each unique Bearer token gets its own quota counter. If Client A uses token abc123, they get 60 requests per minute. If Client B uses token def456, they also get 60 requests per minute independently. This isolation is critical: it prevents one client from monopolizing your API. If Client A burns through all 60 requests, Client B isn't affected.

The fallback to $request->ip() handles edge cases: internal health checks without tokens, or public endpoints that don't require authentication. They're rate-limited by IP address instead, preventing a single machine from overwhelming the server.

Rate limiting is entirely cache-based. When a request arrives, the middleware checks the cache key api-token:{token} and increments a counter. If the counter exceeds 60 within the minute window, the request is rejected with HTTP 429. The cache backend (database, Redis, file system, etc.) automatically expires the counter after 1 minute, resetting the quota for the next window.

This is why your cache configuration matters. If you're using a database cache on an API with high traffic, cache operations become a bottleneck. Redis is dramatically faster. For most production APIs, Redis or Memcached is the right choice for rate limiting.

When a client hits the rate limit:

  1. Laravel throws a ThrottleRequestsException
  2. The exception is caught in your exception handler (see bootstrap/app.php)
  3. The middleware returns a 429 Too Many Requests response
  4. The client receives an error and should stop sending requests

Importantly: rate limiting stops the request before your controller ever runs. You don't process the request, query your database, or call your external APIs. You just reject it immediately. This is efficient and protects your backend from the damage a flood of requests could cause.

Smart API clients don't rely on the status code to know they're being rate-limited. They proactively check the rate limit headers (provided by AddRateLimitHeadersMiddleware) and back off before hitting the limit. They see X-RateLimit-Remaining: 5 and know to slow down. They see X-RateLimit-Reset: 1708899234 and know when their quota resets.

This is the difference between a reactive client (waits to get a 429, then backs off) and a proactive client (checks headers, backs off before hitting the limit). Your API headers make proactive clients possible.

The 60 requests per minute limit is reasonable for most internal APIs, but you can adjust it based on your needs. For higher throughput, change it to Limit::perMinute(300). For stricter limits, change it to Limit::perMinute(10). You can also use perSecond(), perHour(), or custom time windows.

If you need different limits for different endpoints, you can define multiple limiters:

RateLimiter::for('api-token', function (Request $request) {
    return Limit::perMinute(60)->by($request->bearerToken() ?: $request->ip());
});

RateLimiter::for('api-token-strict', function (Request $request) {
    return Limit::perMinute(10)->by($request->bearerToken() ?: $request->ip());
});

Then use 'throttle:api-token-strict' for endpoints that are more expensive or sensitive.

When rate limits are exceeded, an exception is reported to Nightwatch (your error tracking system):

if ($exception instanceof \Illuminate\Http\Exceptions\ThrottleRequestsException) {
    Nightwatch::warning('Rate limit exceeded', [
        'exception' => class_basename($exception),
    ]);
}

This gives you visibility into which clients are hitting limits and how often. If a client repeatedly hits the limit, it might indicate misconfiguration on their end, or they might be attacking your API. Either way, you have the data to investigate and respond.