The PHP CRUD API Generator includes a built-in rate limiting system to prevent API abuse and ensure fair usage across all clients. Rate limiting is configurable, intelligent, and production-ready.
- ✅ Flexible Configuration - Customize limits per use case
- ✅ Smart Identification - Uses user, API key, or IP address
- ✅ Standard Headers - Follows RFC 6585 (X-RateLimit-*)
- ✅ File-Based Storage - No external dependencies required
- ✅ Auto-Cleanup - Prevents storage bloat
- ✅ Easy to Extend - Swap file storage for Redis/Memcached
Edit config/api.php to configure rate limiting:
'rate_limit' => [
'enabled' => true, // Enable/disable rate limiting
'max_requests' => 100, // Maximum requests per window
'window_seconds' => 60, // Time window in seconds
'storage_dir' => __DIR__ . '/../storage/rate_limits', // Storage location
],| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable or disable rate limiting globally |
max_requests |
int | 100 |
Maximum number of requests allowed per window |
window_seconds |
int | 60 |
Time window in seconds (sliding window) |
storage_dir |
string | sys_get_temp_dir() |
Directory to store rate limit data |
Rate limits are applied per identifier in this priority order:
-
Authenticated User (most accurate)
- Format:
user:username - Used when: User is authenticated via Basic Auth or JWT
- Format:
-
API Key (for API key authentication)
- Format:
apikey:hash(key) - Used when: Client authenticates with API key
- Format:
-
IP Address (fallback)
- Format:
ip:xxx.xxx.xxx.xxx - Used when: No authentication or as fallback
- Supports
X-Forwarded-ForandX-Real-IPheaders
- Format:
The rate limiter uses a sliding window algorithm:
Window: 60 seconds
Max: 100 requests
Timeline:
0s → Request 1-50
30s → Request 51-100
31s → ❌ Rate limited (100 requests in last 60s)
61s → ✅ Allowed (requests from 0s expired)
Benefits:
- More accurate than fixed windows
- Prevents burst attacks at window boundaries
- Fair distribution of requests over time
All API responses include rate limit headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1729512345
X-RateLimit-Window: 60| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the window |
X-RateLimit-Remaining |
Number of requests remaining |
X-RateLimit-Reset |
Unix timestamp when the rate limit resets |
X-RateLimit-Window |
Time window in seconds |
When the rate limit is exceeded, clients receive:
Status Code: 429 Too Many Requests
Headers:
Content-Type: application/json
Retry-After: 42
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1729512387
X-RateLimit-Window: 60Response Body:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again in 42 seconds.",
"retry_after": 42,
"reset_at": "2025-10-21 14:33:07",
"limit": 100,
"window": 60
}async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, options);
// Check rate limit headers
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`Rate Limit: ${remaining}/${limit} remaining`);
if (response.status === 429) {
const data = await response.json();
const retryAfter = data.retry_after;
console.warn(`Rate limited. Retry in ${retryAfter} seconds.`);
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
// Retry the request
return apiRequest(url, options);
}
return response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Usage
apiRequest('http://localhost/index.php?action=list&table=users')
.then(data => console.log(data))
.catch(err => console.error(err));import requests
import time
def api_request(url, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url)
# Check rate limit headers
limit = response.headers.get('X-RateLimit-Limit')
remaining = response.headers.get('X-RateLimit-Remaining')
print(f"Rate Limit: {remaining}/{limit} remaining")
if response.status_code == 429:
data = response.json()
retry_after = data.get('retry_after', 60)
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
continue
return response.json()
raise Exception("Max retries exceeded")
# Usage
data = api_request('http://localhost/index.php?action=list&table=users')
print(data)<?php
function apiRequest($url, $maxRetries = 3) {
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Parse rate limit headers
preg_match('/X-RateLimit-Remaining: (\d+)/', $headers, $matches);
$remaining = $matches[1] ?? 'unknown';
echo "Rate Limit: $remaining remaining\n";
if ($httpCode === 429) {
$data = json_decode($body, true);
$retryAfter = $data['retry_after'] ?? 60;
echo "Rate limited. Waiting {$retryAfter} seconds...\n";
sleep($retryAfter);
continue;
}
return json_decode($body, true);
}
throw new Exception('Max retries exceeded');
}
// Usage
$data = apiRequest('http://localhost/index.php?action=list&table=users');
print_r($data);You can implement custom rate limits for specific actions:
// In Router.php (custom modification)
$identifier = $this->getRateLimitIdentifier();
// Different limits for different actions
$limits = [
'list' => ['max' => 200, 'window' => 60], // 200/min for reads
'create' => ['max' => 20, 'window' => 60], // 20/min for creates
'update' => ['max' => 50, 'window' => 60], // 50/min for updates
'delete' => ['max' => 10, 'window' => 60], // 10/min for deletes
];
$action = $query['action'] ?? 'list';
$limit = $limits[$action] ?? ['max' => 100, 'window' => 60];
if (!$this->rateLimiter->checkLimit($identifier, $limit['max'], $limit['window'])) {
$this->rateLimiter->sendRateLimitResponse($identifier);
}// In Router.php (custom modification)
$identifier = $this->getRateLimitIdentifier();
$user = $this->auth->getCurrentUser();
// Skip rate limiting for admin users
if ($user === 'admin' || in_array($user, ['trusted_user1', 'trusted_user2'])) {
// Proceed without rate limiting
} else {
if (!$this->rateLimiter->checkLimit($identifier)) {
$this->rateLimiter->sendRateLimitResponse($identifier);
}
}Replace file-based storage with Redis for better performance:
// Create src/RedisRateLimiter.php
class RedisRateLimiter extends RateLimiter
{
private \Redis $redis;
public function __construct(array $config = [])
{
parent::__construct($config);
$this->redis = new \Redis();
$this->redis->connect('127.0.0.1', 6379);
}
protected function getRequests(string $identifier): array
{
$key = 'ratelimit:' . hash('sha256', $identifier);
$data = $this->redis->get($key);
return $data ? unserialize($data) : [];
}
protected function saveRequests(string $identifier, array $requests): bool
{
$key = 'ratelimit:' . hash('sha256', $identifier);
return $this->redis->setex(
$key,
$this->windowSeconds + 10, // TTL with buffer
serialize($requests)
);
}
}Add to a cron job or scheduled task:
<?php
// cleanup.php
require_once __DIR__ . '/vendor/autoload.php';
$config = require __DIR__ . '/config/api.php';
$rateLimiter = new \App\RateLimiter($config['rate_limit'] ?? []);
// Clean up files older than 1 hour
$deleted = $rateLimiter->cleanup(3600);
echo "Deleted $deleted old rate limit files\n";Cron (Linux/macOS):
# Run every hour
0 * * * * /usr/bin/php /path/to/cleanup.phpTask Scheduler (Windows):
# Run every hour
schtasks /create /tn "API Rate Limit Cleanup" /tr "php d:\path\to\cleanup.php" /sc hourlyPros:
- ✅ No external dependencies
- ✅ Easy to set up
- ✅ Works everywhere
Cons:
⚠️ Disk I/O overhead⚠️ Not ideal for high-traffic APIs (>1000 req/sec)
Recommendation: Suitable for most use cases up to medium traffic.
Pros:
- ✅ In-memory (extremely fast)
- ✅ Built-in expiration
- ✅ Distributed support
Cons:
⚠️ Requires additional service⚠️ More complex setup
Recommendation: Use for high-traffic APIs (>1000 req/sec).
File-based storage performance (tested on SSD):
| Concurrent Users | Requests/sec | Avg Response Time | Rate Limit Overhead |
|---|---|---|---|
| 10 | 500 | 20ms | +2ms |
| 50 | 1,200 | 45ms | +3ms |
| 100 | 1,800 | 80ms | +5ms |
Note: Overhead is minimal for most applications. Consider Redis for >2000 req/sec.
Check:
- Is
rate_limit.enabledset totruein config? - Does the storage directory exist and have write permissions?
- Check error logs for filesystem errors
Solution:
Increase max_requests or window_seconds in config:
'rate_limit' => [
'max_requests' => 200, // Increased from 100
'window_seconds' => 60,
],Solution: Run cleanup script regularly (see Maintenance section above).
Problem: File-based storage is per-server.
Solution: Use Redis with centralized storage:
// All servers point to same Redis instance
$redis->connect('redis-server.internal', 6379);-
Always enable in production
'rate_limit' => ['enabled' => true]
-
Adjust limits based on API usage
- Analyze your API usage patterns
- Set reasonable limits to prevent abuse while allowing legitimate use
-
Monitor rate limit violations
- Log 429 responses
- Alert on suspicious patterns
-
Use HTTPS
- Prevents IP spoofing
- Protects API keys in transit
-
Combine with authentication
- Rate limiting alone is not enough
- Use with API keys or JWT tokens
Q: Will rate limiting slow down my API? A: Overhead is minimal (<5ms per request with file storage). Use Redis for high-traffic APIs.
Q: Can I have different limits for different users? A: Yes! See "Advanced Usage" → "Custom Rate Limits Per Action" above.
Q: What happens if storage directory is deleted? A: Rate limits reset. The directory is auto-created on next request.
Q: Can I disable rate limiting for testing?
A: Yes, set 'enabled' => false in config or create separate config for testing.
Q: How do I monitor rate limit usage?
A: Check the X-RateLimit-* headers in responses or add custom logging.
Planned features:
- Redis/Memcached storage adapters
- Rate limit by geographic location
- Dynamic rate limits based on server load
- Admin dashboard for monitoring
- GraphQL support
Built by BitHost | Report Issues