Skip to content

Rate Limiting

stdOWL edited this page Jun 22, 2026 · 2 revisions

Rate Limiting

The API limits how often each client can call it — to absorb traffic bursts and block abuse such as password-guessing on the login endpoint. It uses a token-bucket limiter implemented as a WebFilter that runs before Spring Security, so excess traffic is rejected before any authentication or business logic runs.

The idea: a token bucket

Each client has a bucket that holds up to N tokens:

  • Every request takes one token.
  • Tokens refill continuously at a fixed rate (N tokens per period).
  • If a token is available, the request is allowed; if the bucket is empty, the request is rejected.

This permits short bursts (up to the bucket's capacity) while capping the sustained rate to the refill rate.

flowchart TD
  R["Request"] --> X{"Excluded path? (docs, health)"}
  X -->|Yes| P["Pass through"]
  X -->|No| K["Build key: tier + client IP"]
  K --> C{"Token available?"}
  C -->|Yes| A["Consume token, allow, add X-RateLimit-Remaining"]
  C -->|No| D["Reject with 429 + Retry-After"]
Loading

Two tiers

The limit depends on the endpoint:

Tier Applies to Limit
auth (strict) /api/auth/** ~10 / minute — slows brute-force and credential stuffing
general everything else ~100 / minute

Documentation and health endpoints (/v3/api-docs, /swagger-ui, /webjars, /actuator) are excluded entirely.

Per-client buckets

The bucket key is tier + client IP, so each client is limited independently — one noisy client cannot exhaust everyone else's allowance.

The client IP comes from the connection's remote address. Behind a trusted proxy you can set ratelimit.trust-forwarded-for=true to use the first X-Forwarded-For entry instead — only enable this when a known proxy sets that header, because clients can otherwise spoof it to dodge the limit.

Backend (where the buckets live)

RateLimiter is pluggable:

  • in-memory — for a single instance (local / dev).
  • Redis — shared across instances; the check-refill-consume step runs atomically (a Lua script) so every instance agrees on the count.

Selected with RATELIMIT_BACKEND=memory|redis (deployed profiles use Redis).

Responses

  • Allowed → the request proceeds; every response carries an X-RateLimit-Remaining header.
  • Exceeded429 Too Many Requests with an RFC 7807 application/problem+json body and a Retry-After header telling the client when to retry.
  • Backend unavailable → the filter fails open (allows the request), so a broken limiter does not take the whole API down.

Configuration

Setting Purpose Default
RATELIMIT_ENABLED turn the limiter on/off true
RATELIMIT_BACKEND memory or redis memory (local)
RATELIMIT_TRUST_XFF trust X-Forwarded-For for the client IP false
ratelimit.auth / ratelimit.general per-tier capacity + refill period 10 / min, 100 / min

See Architecture for how this fits into the overall request flow.

Clone this wiki locally