Rate Limiting
The backend uses IP-based rate limiting to protect sensitive endpoints from brute force attacks and abuse. It is built with Bucket4j (token bucket algorithm) and Redis for distributed bucket storage.
How It Works
A custom @RateLimit annotation is applied to controller methods that need throttling:
@RateLimit(requests = 10, duration = 60)
@PostMapping("/authenticate")
public ResponseEntity<Void> authenticate(...) { ... }requests— maximum number of requests allowed in the window.duration— window size in seconds.
When a request comes in, the RateLimitInterceptor checks if the handler method has a @RateLimit annotation. If it does, a token bucket is resolved for the client’s IP and endpoint combination. If the bucket is exhausted, a 429 Too Many Requests response is returned with a Retry-After header.
Client IP Resolution
The client IP is resolved via HttpRequestUtil.resolveClientIp(). It checks the configured server.client-ip-header first (a CDN-specific header such as CF-Connecting-IP for Cloudflare), falling back to request.getRemoteAddr(). In production, Tomcat’s RemoteIpValve (server.forward-headers-strategy: native) ensures getRemoteAddr() reflects the real client IP from X-Forwarded-For set by Traefik. See the Deploy page for configuration details.
Rate-Limited Endpoints
| Endpoint | Limit |
|---|---|
POST /auth/authenticate | 10 req / 60s |
POST /auth/register | 5 req / 60s |
POST /auth/tokens/refresh | 10 req / 60s |
POST /auth/password/forgot | 3 req / 60s |
POST /auth/password/reset | 5 req / 60s |
POST /auth/verification-email | 3 req / 60s |
POST /auth/verify-email | 5 req / 60s |
POST /users/username/availability | 10 req / 60s |
POST /users/email/availability | 10 req / 60s |
PATCH /profile/password | 5 req / 60s |
Bucket Isolation
Each rate limit bucket is scoped to a specific endpoint + client IP combination. This means:
- Different endpoints have independent limits — exhausting the login limit does not affect forgot-password.
- Different IPs have independent limits — one client being throttled does not affect others.
Configuration
Rate limiting is enabled by default. It can be disabled by setting rate-limit.enabled to false in application.yml. This is used in the test profile to prevent interference with integration tests.
Storage
Buckets are stored in Redis via Bucket4j’s Lettuce integration. Redis keys are namespaced as rate-limit:<Controller>:<method>:<ip> and expire automatically using a basedOnTimeForRefillingBucketUpToMax strategy — keys live only as long as the longest configured rate limit window. Since buckets are stored in the same Redis instance used for token storage, no additional infrastructure is required. This also makes rate limiting work correctly across multiple API instances.