[ GUIDE ]

Monitoring rate limits in Laravel

Track 429s, identify misbehaving consumers, and use rate-limit state as an early-warning signal for attacks and bad integrations.

QUICK ANSWER

How do I monitor rate limits in Laravel?

Track 429 response rate per route in your APM. Laravel's throttle middleware returns 429 when limits are hit — a spike indicates a misbehaving consumer, bot, or attack. Drill in by IP, API key, or user_id to find the source. For distributed setups across multiple servers, use Redis as the rate limiter cache store so limits are enforced globally, not per-server. NightOwl surfaces 429s as part of its request monitoring; filter by status code.

Updated · 2026-06-06

A 429 is the most underused signal in a Laravel app. Most teams set up throttling once, confirm it returns the right status code, and never look at it again. But the rate of 429s, broken down by route, is one of the earliest warnings you get for a whole class of problems — credential stuffing, a customer's integration stuck in a retry loop, a bot farming password resets. It's a security-and-reliability signal hiding inside a status code.

Setting up Laravel rate limits

app/Providers/AppServiceProvider.php

php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function ($request) {
        $key = $request->header('X-Api-Key') ?? $request->ip();
        return Limit::perMinute(100)->by($key);
    });

    RateLimiter::for('login', function ($request) {
        return [
            // 5 per minute per IP
            Limit::perMinute(5)->by($request->ip()),
            // 20 per minute per email (prevents targeted stuffing)
            Limit::perMinute(20)->by($request->input('email')),
        ];
    });
}

routes/api.php

php
Route::middleware('throttle:api')->group(function () {
    Route::get('/users', [UserController::class, 'index']);
    Route::post('/orders', [OrderController::class, 'store']);
});

Route::post('/login', LoginController::class)
    ->middleware('throttle:login');

Track 429s in your APM

429 responses show up in your request telemetry. The value is per-route grouping:

  • /api/* 429 spike → a consumer's retry logic is broken, or an API key is compromised
  • /login 429 spike → credential stuffing attack
  • /password/reset 429 spike → someone farming password reset emails
  • /api/webhooks/* 429 spike → your upstream is retrying because processing is slow

Log rate-limit hits with context

Custom middleware that logs 429s

php
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class LogRateLimitHits
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        if ($response->status() === 429) {
            Log::warning('Rate limit hit', [
                'route' => $request->route()?->uri(),
                'method' => $request->method(),
                'ip' => $request->ip(),
                'api_key' => $request->header('X-Api-Key'),
                'user_id' => auth()->id(),
                'limit_after' => $response->headers->get('X-RateLimit-Remaining'),
            ]);
        }

        return $response;
    }
}

Distributed rate limiting with Redis

On a multi-server fleet, Laravel's default cache-store rate limiter doesn't sync across servers — each server has its own counter:

.env

bash
CACHE_STORE=redis
REDIS_HOST=your-redis-host
REDIS_PORT=6379

# Optional: use a dedicated connection for rate limits
# so they're not evicted by general cache pressure:
# CACHE_RATE_LIMITER_CONNECTION=ratelimit

With Redis, increment+check operations are atomic and global. 100 req/min on a 3-server fleet means 100 total, not 300.

Alerting thresholds

Route pattern Alert at Likely cause
/login10+ 429s/minCredential stuffing
/api/*5x baseline sustained 10minRunaway client retries
/password/*3+ 429s/minPassword-reset farming
/api/webhooks/*any sustainedUpstream retry storm from slow processing

The proxy trap

One subtle failure mode: if your app sits behind a load balancer or CDN and you key the limiter by $request->ip(), every request can look like it came from the proxy's IP. The limiter then throttles all your users as if they were one client, or — if the proxy rotates IPs — never throttles anyone. Laravel only reads the real client address from X-Forwarded-For when you've configured TrustProxies correctly. So if your rate limits behave nothing like you expect in production but work fine locally, check trusted proxies before you touch the limit values themselves.

THE EASY WAY

NightOwl surfaces 429s as part of request monitoring

Filter the requests dashboard by status=429 per route. Drill into specific rate-limited requests to see which IP, API key, or user hit the ceiling. Set alerts on 429 rate thresholds via any configured alert channel.

bash
composer require nightowl/agent
php artisan nightowl:install

Frequently asked questions

How do I monitor rate limits in Laravel?

Laravel's RateLimiter returns 429 responses when limits are exceeded. Track the 429 rate per route in your APM. A spike usually indicates a specific consumer retrying aggressively or a bot; drill into the rate-limited requests by IP, API key, or user_id to find the source.

What's the difference between Laravel's throttle middleware and the RateLimiter facade?

The throttle middleware (Route::middleware('throttle:60,1')) is the high-level HTTP-routing use case — 60 requests per minute per keyed user. The RateLimiter facade is the programmatic API you use in controllers, jobs, or anywhere else you want to enforce a limit outside HTTP. Both ultimately use the same underlying Illuminate\Cache\RateLimiter.

How do I rate-limit per API key instead of per IP?

In a service provider, register a named limiter keyed by the API key: RateLimiter::for('api', fn($request) => Limit::perMinute(100)->by($request->header('X-Api-Key') ?? $request->ip())). Then apply via throttle:api in your route group.

Should I alert on high 429 rates?

Yes, but at the right threshold. A few 429s per minute are normal (someone spamming a form). Alert when 429 rate jumps 5x+ above baseline for 10+ minutes — that's a misbehaving integration or attack. Route-level alerts beat global: a spike on /api/login means credential stuffing, on /api/webhooks/stripe means your webhook handler is slow.

How do I know if my rate limits are too aggressive?

Track rate-limited users/keys. If the same legitimate user or API key repeatedly hits your limits, they're either being throttled too hard or you're missing proper per-tier quotas. Look at the distribution — if 99% of traffic is well under the limit and 1% is being limited, the limits are probably fine. If 30% is being limited, they're too strict.

Can I use Redis for distributed rate limiting in Laravel?

Yes — set CACHE_STORE=redis and the RateLimiter uses Redis atomically. This is required if you have multiple web servers, otherwise each server has its own in-memory counter and limits become per-server (effectively 3x more permissive on a 3-server fleet).

How do I communicate rate-limit state to API consumers?

Laravel includes X-RateLimit-Limit and X-RateLimit-Remaining headers automatically on throttled routes. Well-behaved clients read these and back off. Also return Retry-After on 429 responses. Document these headers in your API docs so consumers can build proper backoff into their SDKs.

PRICING

Flat pricing. No event caps. No per-seat fees.

14-day free trial, no credit card. Your PostgreSQL, your data.

HOBBY

$5 /month

1 app · 14 days lookback · all Laravel events

TEAM

$15 /month

Up to 3 connected apps · unlimited environments · all Laravel events

AGENCY

$69 /month

Unlimited apps · unlimited agent instances · same flat rate at any traffic

Related