Skip to content
CommonLayer
Documentation menu
On this page

ADR-0019: The global rate limit runs after authentication and fails open

Status: Accepted (WP-02). Amended by WP-70a: a limiter outage now falls back to per-process limits instead of failing open (amended 2026-11-10).

Context

The plan placed the global rate limit before authentication in the middleware stack. But the limits are per principal for authenticated requests (600 per minute) and per IP for anonymous ones (120 per minute); the principal is only known after authentication.

Decision

  • Run the global API rate limit directly after the authentication middleware. The middleware order is: request id → trace and metrics → panic catcher → security headers → body limit → timeout → CORS → authentication with the global rate limit → CSRF → authentication gate → router.
  • Use fixed windows per (bucket, key). With Valkey configured, one Lua script performs INCR and PEXPIRE atomically; without it an in-memory limiter is used (development and tests). Per-account login keys are SHA-256 hashes of the normalized login, so no email address is stored in Valkey.
  • If the limiter backend is unavailable, the request is allowed and the failure is logged ("fail open").
  • The global API buckets apply to /api/ paths only; webhooks and the registry token realm have their own buckets.

Consequences

  • Authentication work (one session or token lookup) happens before an anonymous flood is limited; that cost is bounded by the body limit and timeout layers.
  • A Valkey outage removes rate limiting. Login brute force is still bounded by the account lockout stored in PostgreSQL, but other buckets (registration, password reset, API) are unprotected during the outage. This is listed as an open item in the threat model; an alert on limiter errors does not exist yet.
  • Configuration requires cache.url in production so that limits are shared across API replicas.

Amendment, 2026-11-10

  • "Fails open" no longer holds. Since WP-70a a failed Valkey call is decided by an in-memory limiter in the same process (FallbackRateLimiter in crates/commonlayer-auth/src/adapters/rate_limit.rs): limits stay in force, but per process, so N API replicas allow up to N times a limit during an outage. Every fallback counts in commonlayer_rate_limiter_fallbacks_total{bucket}, /readyz reports rate_limiter as degraded, and the alert RateLimiterDegraded fires. Failing closed was rejected because it would turn a cache outage into a login outage.
  • The client address used for per-IP buckets is taken from X-Forwarded-For only when the direct peer is a declared trusted proxy (ADR-0040).