Rate Limiting
Two independent rate-limiting layers apply, for different purposes, and they don't share the same limitation.
Two independent layers apply to every request, and they share a client-visible error code but not the same underlying limitation — worth reading past just "both return 429."
Layer 1: engine-level, per user
Built into the cryden engine itself, applied to login, signup, and magic-link requests specifically. Default: RATE_LIMIT_ATTEMPTS attempts (10) per RATE_LIMIT_WINDOW_SECONDS window (60).
Distinct from account lockout: this only slows rapid attempts within a short window, it does not lock anything. Account lockout is a separate, database-backed mechanism — see design-decisions.md.
Can be made to share state across instances. In-memory by default, but setting REDIS_URL swaps in the engine's Redis-backed limiter, sharing one window across every replica.
Layer 2: edge-level, per IP, whole API
Implemented in this repository (httpapi/ratelimit.go), applied to every request regardless of endpoint. Default: 100 requests per minute per IP, tunable with EDGE_RATE_LIMIT. OPTIONS preflight requests are never counted — browsers issue these automatically, and they shouldn't consume a caller's real budget.
Cannot be made to share state across instances. This is documented as an honest, permanent limitation of this specific layer, not a gap waiting on a config flag: it is in-memory and per-process with no Redis-backed alternative at all, unlike Layer 1. Each running instance keeps its own independent count, so a deployment behind a load balancer with more than one instance ends up with an effective limit closer to EDGE_RATE_LIMIT × instance count than the configured number. If that matters for a given deployment, the fix is putting a real rate limiter in front at the infrastructure level (a CDN, an API gateway) — this layer was never meant to be the last word on abuse at scale, it's a coarse backstop.
Client IP, for both layers
Read from X-Forwarded-For (its first entry) when present, falling back to the raw connection address otherwise. This is also what shows up in audit records and named-session labels — the same IP resolution feeds all three, so they can't disagree with each other about which address a request came from.
Both layers, one error code
Both surface 429 rate_limited to the client. The distinction between "the engine's per-user limiter fired" and "the edge per-IP limiter fired" only matters for server-side debugging — client-side error handling can treat both identically.