Rate limiting
Rack::Attack (#382), configured in config/initializers/rack_attack.rb.
Counters live in Rails.cache (Redis in production, memcached in dev). If the
cache store is down, both stores’ error handling makes throttling fail open —
requests succeed uncounted; nothing 500s.
| Throttle | Key | Limit |
|---|---|---|
logins/ip |
IP | 10/min on POST /users/sign_in |
logins/email |
submitted email | 20/hour on POST /users/sign_in |
signups/ip |
IP | 10/hour on POST /users |
password_resets/ip |
IP | 5/hour on POST /users/password |
contacts/ip |
IP | 5/hour on POST /contacts |
audio/user |
user id (IP when signed out) | 30/min on POST /api/resources/audio — every uncached hit is a paid Polly call |
Safelists: /up (Kamal health checks) and /admin + /sidekiq.
Throttled requests get a plain-text 429 with a Retry-After header. Every
throttle hit logs a [Rack::Attack] warning with the matched rule, IP, and
path.
Rack::Attack is disabled in the test environment (the suite posts to auth
endpoints freely); test/integration/rack_attack_test.rb enables it with a
MemoryStore per test.