Rate Limiting
This page documents the rate limiting configuration to protect the API against abuse.
Overview
Rate limiting restricts the number of requests a client can make within a given period, protecting against:
- Brute force attacks
- Denial of service (DoS)
- Abusive scraping
- Server overload
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Client │ Request │ Rate Limiter │ Allow │ Backend │
│ │ ------> │ (Redis) │ ------> │ API │
│ │ │ │ │ │
│ │ <------ │ │ │ │
│ │ 429 Too │ │ │ │
│ │ Many │ │ │ │
└─────────────────┘ └─────────────────┘ └──────────────┘
Configuration
Bundle Installation
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
# Global API limit
api_general:
policy: sliding_window
limit: 100
interval: '1 minute'
# Login limit (brute force protection)
api_login:
policy: fixed_window
limit: 5
interval: '1 minute'
# Per-IP limit
api_per_ip:
policy: sliding_window
limit: 1000
interval: '1 hour'
# Export limit (resource intensive)
api_export:
policy: fixed_window
limit: 10
interval: '1 hour'
Environment Variables
# Enable rate limiting
RATE_LIMITING_ENABLED=true
# Storage backend
RATE_LIMITER_STORAGE=redis
# Redis connection
REDIS_URL=redis://localhost:6379
Policies
Fixed Window
Counts requests in a fixed window (e.g., per minute).
|---1 min---|---1 min---|---1 min---|
| 100 req | 100 req | 100 req |
Advantage: Simple, predictable Disadvantage: Bursts possible at boundaries
Sliding Window
Sliding window based on current time.
|--------1 min--------|
10:00:30 10:01:30
Advantage: Traffic smoothing Disadvantage: More resources
Token Bucket
Token bucket with regeneration.
Advantage: Allows controlled bursts Disadvantage: Complex configuration
Implementation
Controller with Rate Limiting
use Symfony\Component\RateLimiter\RateLimiterFactory;
class AuthController extends AbstractController
{
public function __construct(
private RateLimiterFactory $loginLimiter
) {}
#[Route('/api/auth/login', methods: ['POST'])]
public function login(Request $request): Response
{
$limiter = $this->loginLimiter->create($request->getClientIp());
if (!$limiter->consume()->isAccepted()) {
return $this->json([
'success' => false,
'error' => 'Too many attempts. Try again in 1 minute.'
], 429);
}
// Login logic...
}
}
Global Listener
class RateLimitListener implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$limiter = $this->apiLimiter->create($request->getClientIp());
if (!$limiter->consume()->isAccepted()) {
$response = new JsonResponse([
'success' => false,
'error' => 'Rate limit exceeded',
'retry_after' => $limiter->getInfo()->getRetryAfter()->getTimestamp()
], 429);
$response->headers->set('Retry-After', $limiter->getInfo()->getRetryAfter()->getTimestamp());
$event->setResponse($response);
}
}
}
HTTP Headers
Normal Response
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1718198400
Limit Reached
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1718198400
{
"success": false,
"error": "Rate limit exceeded",
"retry_after": 45
}
Limits by Endpoint
| Endpoint | Limit | Interval | Reason |
|---|---|---|---|
/api/auth/login | 5 | 1 min | Brute force protection |
/api/auth/magic-link | 3 | 5 min | Avoid email spam |
/api/export/* | 10 | 1 hour | Resource intensive |
/api/* (global) | 100 | 1 min | General protection |
Exemptions
Certain clients can be exempted:
rate_limiter:
api_general:
# ...
exempt:
- 127.0.0.1
- '192.168.1.0/24' # Internal network
By Role
if ($this->isGranted('ROLE_API_UNLIMITED')) {
return; // No limit
}
Monitoring
Prometheus Metrics
# Limited requests
rate_limit_exceeded_total{endpoint="/api/auth/login"} 42
# Requests near limit
rate_limit_near_threshold{endpoint="/api/*"} 156
Grafana Alerts
- Alert if > 100 blocked requests / minute
- Alert if same IP blocked > 10 times
Redis Storage
Configuration
services:
rate_limiter.storage.redis:
class: Symfony\Component\RateLimiter\Storage\CacheStorage
arguments:
- '@cache.rate_limiter'
framework:
cache:
pools:
cache.rate_limiter:
adapter: cache.adapter.redis
provider: 'redis://localhost:6379'
Advantages
- Shared between instances (cluster)
- Counter persistence
- High performance
Best Practices
:::tip Recommendations
- Adapted limits: Adjust based on actual usage
- Strict login: Low limit for auth endpoints
- Clear headers: Inform clients of limits
- Monitoring: Monitor blocked requests
- Limited exemptions: Only exempt what's necessary :::
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Legitimate blocks | Limit too low | Increase limit |
| Redis down | Storage unavailable | Memory fallback |
| Shared IP | NAT/Proxy | Use X-Forwarded-For |