Skip to content

Rate limiting

Any route with Laravel’s throttle middleware is documented with a 429 Too Many Requests response and the accompanying Retry-After and X-RateLimit-* headers. This is on by default — no package or configuration required.

Docuccino reads the throttle configuration from the middleware:

Middleware Documented limit
throttle:60,1 60 requests per 1 minute.
throttle:60 60 requests per minute (Laravel’s default decay).
throttle:60,0.5 60 per 30 seconds — a fractional decay resolves to whole seconds.
throttle (bare, no arguments) Laravel’s middleware default — 60 requests per minute.
throttle:10|60 (guest|authenticated) The authenticated allowance (60) is documented; the response description records the lower unauthenticated limit (10 per window).
throttle:api (a named limiter with a single limit) The concrete numbers, read from the limiter’s RateLimiter::for closure — e.g. fn () => Limit::perMinute(60) documents 60 per minute.
throttle:api (a dynamic, conditional, or custom-response limiter) The 429 response and headers, without concrete numbers — plus an info diagnostic.

Both the short throttle alias and the ThrottleRequests::using('api') / ::with(...) forms are recognized, including the FQCN strings they render to and the Redis variant (ThrottleRequestsWithRedis). When a route stacks several throttle middlewares, a single 429 is documented from the first, with a rate-limit.multiple-throttles info diagnostic noting the others are enforced independently but not separately represented.

routes/api.php
Route::middleware('throttle:60,1')->get('/invoices', [InvoiceController::class, 'index']);

X-RateLimit-Limit carries the allowance as its example and Retry-After carries the window length in seconds; the two runtime-varying headers are documented as integers with no example, because there is no honest static value for them. The body is always the small { "message": string } JSON payload Laravel’s ThrottleRequests returns.

A named limiter (throttle:api) registers its rate inside a closure — RateLimiter::for('api', fn () => ...). Docuccino never executes that closure, but it reads it: when the limiter returns a single Limit, its numbers flow straight into the 429.

// A limiter Docuccino folds — the 429 documents 60 requests per minute.
RateLimiter::for('api', fn (Request $request) => Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()));

All of Limit::perSecond, perMinute, perMinutes, perHour and perDay are recognized (note perMinutes($decayMinutes, $maxAttempts) takes its decay first), and a trailing ->by(...) partition key is ignored — it changes who the limit applies to, not what the limit is.

Docuccino keeps the numberless 429 — plus a rate-limit.dynamic-limit info diagnostic — when the limit can’t be read statically, rather than guessing. That covers a limiter that:

  • returns different limits down different branches (a conditional or multi-return closure);
  • computes its rate from a non-literal value (Limit::perMinute($request->user()->rate));
  • is unlimited (Limit::none()) or returns an array of limits;
  • attaches a custom response body (->response(...)).

The same diagnostic fires — naming the limiter — when a route throttles on a name that has no RateLimiter::for registration at all, which is a genuinely useful thing to find out at build time.

Inline limits (throttle:60,1) always carry their numbers straight through.

The integration is always on, and has one switch:

// config/docuccino.php → documents.default.integrations
'rate_limit' => ['enabled' => false], // stop documenting 429s in this document

Turning it off for a document emits one integration.disabled info diagnostic per build, so the choice stays visible in the output rather than looking like a bug.