Skip to content

Rate Limiting (API)

Public-facing rate limits applied to authentication endpoints. For the implementation details (middleware, Redis keys, IP blocking, suspicious-activity heuristics), see Security → Rate Limiting.

Per-endpoint limits

Each limit is per client IP and applies to the API endpoint listed. Counters reset on a sliding window backed by Redis.

Endpoint Limit Window Source
POST /api/v1/auth/register 5 1 hour RateLimiter.RegisterLimit()
POST /api/v1/auth/login 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/google-login 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/line-login 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/apple-login 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/webauthn/login/begin 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/webauthn/login/finish 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/otp/send 5 1 hour RateLimiter.RegisterLimit()
POST /api/v1/auth/otp/verify 10 15 minutes RateLimiter.LoginLimit()
POST /api/v1/auth/reset-password 5 1 hour RateLimiter.ResetPasswordLimit()
Every route under /api/v1/* (baseline) 600 1 minute RateLimiter.APILimit()

These are the values defined in backend/internal/middleware/rate_limiter.go. They match the per-IP counters; multiple users behind one NAT share a counter.

How the limits stack

APILimit() is wired on the /api/v1 group, so every API request counts against the 600/min/IP baseline. Targeted limits (register, login, reset-password) are layered on top of the baseline for those specific endpoints — a registration attempt counts against both the 5/hour register bucket and the 600/min baseline. Routes under /ws (WebSocket) and webhook endpoints are not in the baseline.

Response when limited

A blocked request returns:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "error": "too many login attempts, please try again later (max 10 per 15 minutes)"
}

The exact message varies per endpoint (see the ErrorMessage in RateLimitConfig). The error payload uses the legacy { "error": ... } shape today — clients should treat any 429 as rate-limited regardless of body shape.

Retry-After

429 responses include a Retry-After: <seconds> header set to the window duration for the limit that fired (e.g. Retry-After: 60 for the baseline, Retry-After: 900 for login, Retry-After: 3600 for register). Clients should:

  1. Honor the header — wait at least that many seconds before retrying.
  2. Surface a generic "Try again later" message to the user. Showing the exact countdown is fine.
  3. Not retry the same request automatically — these limits exist to defeat scripted attacks, and silent retries defeat the purpose.

IP blocking escalation

Repeated rate-limit violations lead to an outright IP block, not just per-route 429s. See Security → Rate Limiting for:

  • The auto-block threshold (5 violations within 1 hour → 24-hour block)
  • Suspicious activity heuristics (SQL-injection patterns, path traversal → 48-hour block)
  • How to unblock an IP

Failure mode

If Redis is unavailable, the rate limiter fails open — every request is allowed through. This is intentional: a Redis outage should not lock everyone out of the API. The trade-off is that during an outage, an attacker has free reign on the auth endpoints; recover Redis quickly.