agentleFS
AnyOrgNameAnyOrgName/php-skills/skills/laravel/rate-limiting/SKILL.md

rate-limiting

Skill4 starsChanged 4 months agoMITView on GitHub

Throttle requests and jobs in Laravel — define named limiters with the RateLimiter facade, apply the throttle middleware per route and per user, back limits with Redis for accuracy at scale, return correct 429 responses, and set dynamic per-plan limits. Use when the user needs per-user, per-IP, or dynamic per-plan rate limits defined with the RateLimiter facade; the user is choosing a rate-limit store (Redis vs the default cache) or debugging 429 responses and Retry-After headers; the user wants to throttle requests to a route or API with the throttle middleware or a named limiter. Keywords: rate-limiting, throttle, ratelimiter, middleware, redis, cache, performance, named-limiters, 429, api, input, laravel.

12 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use background-jobs, input-validation and 6 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • YepAPISkill

    background-jobs

    Queue workers with BullMQ/Inngest/Trigger.dev, job retries, dead letter queues, concurrency.

    Also in 3 projects that use this

  • hereshecodesSkill

    input-validation

    Use when processing user input, form data, file uploads, or query parameters

    Also in 3 projects that use this

  • wpankSkill

    caching

    Caching strategies, invalidation, eviction policies, HTTP caching, distributed caching, and anti-patterns. Use when designing cache layers, choosing eviction policies, debugging stale data, or optimizing read-heavy workloads.

    Also in 3 projects that use this

  • SentrySkill

    feature-flags

    Gate a Sentry feature behind a FlagPole feature flag. Use when adding a feature flag, registering a flag in temporary.py, checking a flag from Python or the frontend, enabling a flag in tests, or asking where FlagPole rollout config lives. Trigger on "add a feature flag", "gate this behind a flag", "register a flag", "features.has", "api_expose", "OrganizationFeature", "ProjectFeature", "FlagPole".

    Also in 3 projects that use this

  • xfstudioSkill

    secrets-management

    Implement secure secrets management for CI/CD pipelines using Vault, AWS Secrets Manager, or native platform solutions. Use when handling sensitive credentials, rotating secrets, or securing CI/CD environments.

    Also in 3 projects that use this

  • OpenAISkill

    observability

    Vercel Observability expert guidance — Drains (logs, traces, speed insights, web analytics), Web Analytics, Speed Insights, runtime logs, custom events, OpenTelemetry integration, and monitoring dashboards. Use when instrumenting, debugging, or optimizing application performance and user experience on Vercel.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

9 rules. 0 are common, 6 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 10 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Do not use rate limiting to authenticate or authorize callers.

    line 77
  2. Distinctive:

    Do not use HTTP throttle middleware to pace queued jobs against a rate-limited API.

    line 78
  3. Distinctive:

    Verify: The limiter name resolves and keys by user id with an IP fallback.

    line 131
  4. Very specific:

    Verify: php artisan route:list shows throttle:api on the group and a burst returns 429.

    line 134
  5. Distinctive:

    Verify: Exceeding the limit returns JSON 429 with Retry-After and X-RateLimit-* headers.

    line 137
  6. Very specific:

    Verify: config('cache.default') is redis and counters appear as keys in Redis.

    line 153
  7. Distinctive:

    Verify: The limit trips at the same total across both servers, not once per node.

    line 156
  8. Very specific:

    Use a separate cache prefix or Redis database for limiter keys, and configure trusted proxies so request()->ip() is the real client behind …

    line 157
  9. Distinctive:

    Verify: Limiter keys do not collide with app cache and IP-keyed limits use the real client IP.

    line 159

“Common” is measured: kept by at least 6 in 10 of the 10 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- generated: do not edit — source: knowledge/atoms/laravel/rate-limiting/ --><!-- schema: atomVersion=3 skillVersion=2 graphVersion=1 --><!-- compile: @php-skills/compiler v1.0.0 (deterministic build) -->

Laravel Rate Limiting

When to use

  • The user needs per-user, per-IP, or dynamic per-plan rate limits defined with the RateLimiter facade.
  • The user is choosing a rate-limit store (Redis vs the default cache) or debugging 429 responses and Retry-After headers.
  • The user wants to throttle requests to a route or API with the throttle middleware or a named limiter.

When NOT to use

  • Do not use rate limiting to authenticate or authorize callers; use auth guards and policies for identity, and rate limiting only to cap request volume.
  • Do not use HTTP throttle middleware to pace queued jobs against a rate-limited API; use queue rate-limiting middleware such as RateLimited on the job instead.

Core guidance

  • Key limiters by user id, falling back to IP — In RateLimiter::for, key the limit by the authenticated user id and fall back to the request IP for guests ($request->user()?->id ?: $request->ip()).
  • Define named limiters instead of inline throttle numbers — Register named limiters with RateLimiter::for('api', ...) and apply throttle:api, rather than scattering throttle:60,1 literals across routes.
  • Back rate limits with Redis across multiple servers — When the app runs on more than one web server, set the cache store used by the limiter to Redis so counters are shared, not the per-node file or array store.

Decision Engine

Walk this section before choosing an implementation path.

Where does the app run — many servers, a single server, or a non-HTTP hot path —
and how strict must the shared count be?

├── Single server or an already-shared cache store: → default cache store — the RateLimiter uses the app's configured cache; fine on one
server or when that cache (database, memcached) is already shared and fast enough.

├── A login attempt, command, or method rather than a route: → RateLimiter facade — guard the action with `RateLimiter::attempt` or
`tooManyAttempts`; the throttle middleware only runs on routes, so non-route
paths call the facade directly (still backed by the chosen cache store).

├── Local development or automated tests only: → array store — in-memory counters reset each request lifecycle; use only in local
dev and tests, never in production where counts must persist across requests.

└── Multiple web servers needing one shared, accurate count: → redis store — a shared Redis cache keeps one atomic counter across all servers, so
the global limit is enforced exactly; the default choice for multi-server apps.

Full engine: references/decision-engine.md — Decision Tree, Priority Matrix, Decision Table.

Workflows

Workflow: Named Limiter

Progress:
- [ ] In a service provider's `boot`, call `RateLimiter::for('api', fn (Request $r) =>
Limit::perMinute(60)->by($r->user()?->id ?: $r->ip()))`
- [ ] Attach the limiter with `Route::middleware('throttle:api')` on the route group so
every endpoint in it shares the named policy
- [ ] Chain `->response(fn () => response()->json(['message' => 'Too many requests'],
429))` on the Limit so throttled API calls get a JSON body plus Retry-After
  1. In a service provider's boot, call RateLimiter::for('api', fn (Request $r) => Limit::perMinute(60)->by($r->user()?->id ?: $r->ip())).
    • Verify: The limiter name resolves and keys by user id with an IP fallback.
  2. Attach the limiter with Route::middleware('throttle:api') on the route group so every endpoint in it shares the named policy.
    • Verify: php artisan route:list shows throttle:api on the group and a burst returns 429.
  3. Chain ->response(fn () => response()->json(['message' => 'Too many requests'], 429)) on the Limit so throttled API calls get a JSON body plus Retry-After.
    • Verify: Exceeding the limit returns JSON 429 with Retry-After and X-RateLimit-* headers.

Workflow: Redis Store

Progress:
- [ ] Install phpredis or predis and set `CACHE_STORE=redis` (or a dedicated limiter
store) so the RateLimiter's counters live in Redis
- [ ] Send requests through two app servers behind the load balancer and confirm the
combined count trips the limit, proving the counter is shared in Redis
- [ ] Use a separate cache prefix or Redis database for limiter keys, and configure
trusted proxies so `request()->ip()` is the real client behind a load balancer
  1. Install phpredis or predis and set CACHE_STORE=redis (or a dedicated limiter store) so the RateLimiter's counters live in Redis.
    • Verify: config('cache.default') is redis and counters appear as keys in Redis.
  2. Send requests through two app servers behind the load balancer and confirm the combined count trips the limit, proving the counter is shared in Redis.
    • Verify: The limit trips at the same total across both servers, not once per node.
  3. Use a separate cache prefix or Redis database for limiter keys, and configure trusted proxies so request()->ip() is the real client behind a load balancer.
    • Verify: Limiter keys do not collide with app cache and IP-keyed limits use the real client IP.

Best practices

  • Customize the limiter's response() callback to return a JSON 429 body with a clear message, and rely on the automatic Retry-After and X-RateLimit-* headers so clients can back off
  • For non-route hot paths (login attempts, a console command, a service method) guard the action with RateLimiter::attempt() or tooManyAttempts() instead of the HTTP throttle middleware
  • Return a Limit computed from the caller in the limiter closure (e.g

Anti-patterns

Anti-patternWhy it failsDo instead
One global limit for auth and public endpoints alikeA loose global limit lets credential-stuffing and brute-force attacks run at nearly full speed against auth endpoints.Give sensitive endpoints their own strict limiter keyed by identifier, separate from the general API limit.
Scattering throttle:60,1 literals across routesDuplicated literals drift apart, hide the real policy, and make per-plan limits impossible to reason about.Define one named limiter with RateLimiter::for and apply throttle:name so the policy lives in a single place.
Hand-rolling rate limits with sleeps or DB countersHome-grown throttles race under concurrency, block worker or request threads on<br>sleep, and miss the Retry-After and 429 handling Laravel provides.Use RateLimiter::attempt/tooManyAttempts or the throttle middleware backed by<br>Redis so the counter is atomic and the 429 response is correct.

See references/anti-patterns.md.

Debugging

  1. Keying by user id crashes for guests — Guests have no authenticated user, so an unguarded user-id key either errors or lumps all guests together. Fix: Use ->by($request->user()?->id ?: $request->ip()) so authenticated callers key by id and guests fall back to their IP.
  2. IP limits throttle everyone behind a proxy — Every request appears to come from the proxy, collapsing per-IP limits into a single global bucket. Fix: Configure trusted proxies (and the correct forwarded headers) so the real client IP is used, or key the limiter by user id where a user is authenticated.
  3. throttle:name with no registered limiter passes silently — An unknown named limiter resolves to no limit, so the middleware is effectively a no-op. Fix: Register every named limiter in a service provider's boot and assert in a test that a burst against the route returns 429.
  4. Per-node cache multiplies the effective limit — A non-shared store keeps independent counters, so the global limit is silently multiplied by the server count. Fix: Set the cache (or a dedicated limiter store) to Redis so every server increments the same counter and the cap is enforced globally.

Performance notes

  • Per-node cache multiplies the effective limit — With a file or array cache store and several web servers, each node counts hits locally, so a 60/min limit becomes 60 per server.
  • Back rate limits with Redis across multiple servers — When the app runs on more than one web server, set the cache store used by the limiter to Redis so counters are shared, not the per-node file or array store.

Production checklist

  • Throttled requests return a 429 with Retry-After and X-RateLimit-* headers, and API limiters return JSON rather than an HTML error page.
  • Login, password-reset, and OTP endpoints have their own strict limiter keyed by identifier so credential-stuffing is throttled.
  • Behind a load balancer or CDN, trusted proxies are configured so request()->ip() reflects the real client and IP-keyed limits are not all one address.
  • Authenticated limiters key by user id with an IP fallback for guests, so shared-proxy users are not throttled as one caller.
  • Rate limits are defined as named limiters and applied as throttle:name, not duplicated as throttle:60,1 literals across routes.
  • On multi-server deployments the rate-limit store is Redis (or another shared cache), never a per-node file or array store.

Version notes

  • CACHE_STORE env key naming across versions — Laravel 11 renamed the cache env key to CACHE_STORE (from CACHE_DRIVER in 10.x), which is the store the RateLimiter uses; set it to redis for shared, accurate limits on 10.x–12.x.
  • RateLimiter facade and named limiters from Laravel 8 — The RateLimiter facade with RateLimiter::for named limiters and the Limit::perMinute builder has been available since Laravel 8 and is supported across the 10.x–12.x range; earlier apps configured throttle only inline.

Security

  • Rate-limit auth endpoints and derive keys from trusted input — Put strict, separate limiters on login, password-reset, and OTP endpoints keyed by a stable identifier, and only key limiters by client-supplied values (IP, headers) once trusted proxies are configured so they cannot be spoofed.

Testing

  • Activation and decision behavior are verified in eval/scenarios/laravel/rate-limiting.yaml.

Additional resources

  • references/anti-patterns.md
  • references/core.md
  • references/decision-engine.md
  • references/examples.md
  • references/pitfalls.md
  • references/recipes.md
  • references/version-differences.md

Knowledge graph

Prerequisites: databases/redis
Related: security/rate-limiting

<!-- graph-fragment: graph/fragments/laravel/rate-limiting.yaml -->

Skill contract

Produces: ratelimit.configured
Risk: low · Confidence: 0.9

<!-- contract-fragment: contracts/fragments/laravel/rate-limiting.yaml -->

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.