Rate limiting
This page describes the limits actually active against abuse: sign-in brute force, mass link requests, assistant cost.
What is active
| Protection | Limit | Key | Where |
|---|---|---|---|
| Account lockout | 5 failed sign-ins, then account locked for 30 minutes | account | LoginSecurityService |
| Magic sign-in link | 3 requests per 5-minute window | account | MagicLinkService; 429 response |
| Bemba assistant (application) | 40 questions per sliding 10-minute window | user | assistant limiter; 429 response |
| Public website assistant | 20 questions per 10 minutes, and 500 per day in total | IP address, then global | assistant_public and assistant_public_global limiters |
Limiter counters are stored in the application cache: Redis in production, the file system elsewhere. Sign-in attempts are recorded in the database (IP address, browser, failure reason) and a sign-in from a never-seen address is flagged.
Configuration: backend/config/packages/rate_limiter.yaml.
What is not active
The configuration file also declares, for production, per-IP limiters on the whole API (login, authentication, api_global, api_write, password_reset, export). The code that applies them, RateLimitingSubscriber, is excluded from the services in backend/config/services.yaml: these limits do not apply. Neither Traefik nor Nginx adds rate limiting.
The required packages are installed. Enabling the subscriber means removing its exclusion in services.yaml. Two points to check first: the global limit (100 requests per minute per IP) is low for a whole team behind one office address, and the address Symfony sees must be the client's, not Traefik's (trusted_proxies).
Unlock an account
The lock lifts on its own after 30 minutes. To lift it earlier, an administrator calls POST /api/users/{id}/unlock (the interface has no dedicated button yet), or resets the password with app:reset-user-password, which also lifts the lock. A successful sign-in resets the failure counter.
429 response
The assistant limiters answer 429 Too Many Requests with a readable message. The interface shows it as is; just wait for the window to end.