9.2 KiB
Rate Limiting Policy
Last Updated: November 3, 2025
Status: ACTIVE
Coverage: All public edge functions
Overview
ThrillWiki enforces rate limiting on all public edge functions to prevent abuse, ensure fair usage, and protect against denial-of-service (DoS) attacks.
Rate Limit Tiers
Strict (5 requests/minute per IP)
Use Case: Expensive operations that consume significant resources
Protected Endpoints:
/upload-image- File upload operations- Future: Data exports, account deletion
Reasoning: File uploads are resource-intensive and should be limited to prevent storage abuse and bandwidth exhaustion.
Standard (10 requests/minute per IP)
Use Case: Most API endpoints with moderate resource usage
Protected Endpoints:
/detect-location- IP geolocation service- Future: Public search/filter endpoints
Reasoning: Standard protection for endpoints that query external APIs or perform moderate processing.
Lenient (30 requests/minute per IP)
Use Case: Read-only, cached endpoints with minimal resource usage
Protected Endpoints:
- Future: Cached entity data queries
- Future: Static content endpoints
Reasoning: Allow higher throughput for lightweight operations that don't strain resources.
Per-User (Configurable, default 20 requests/minute)
Use Case: Authenticated endpoints where rate limiting by user ID provides better protection
Protected Endpoints:
/process-selective-approval- 10 requests/minute per moderator- Future: User-specific API endpoints
Reasoning: Moderators have different usage patterns than public users. Per-user limiting prevents credential sharing while allowing legitimate high-volume usage.
Implementation:
const approvalRateLimiter = rateLimiters.perUser(10); // Custom limit
Rate Limit Headers
All responses include rate limit information:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
On Rate Limit Exceeded (HTTP 429):
Retry-After: 45
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
Error Response Format
When rate limit is exceeded, you'll receive:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later.",
"retryAfter": 45
}
HTTP Status Code: 429 Too Many Requests
Client Implementation
Handling Rate Limits
async function uploadImage(file: File) {
try {
const response = await fetch('/upload-image', {
method: 'POST',
body: formData,
});
if (response.status === 429) {
const data = await response.json();
const retryAfter = data.retryAfter || 60;
console.warn(`Rate limited. Retry in ${retryAfter} seconds`);
// Wait and retry
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return uploadImage(file); // Retry
}
return response.json();
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
Exponential Backoff
For production clients, implement exponential backoff:
async function uploadWithBackoff(file: File, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('/upload-image', {
method: 'POST',
body: formData,
});
if (response.status !== 429) {
return response.json();
}
// Exponential backoff: 1s, 2s, 4s
const backoffDelay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, backoffDelay));
} catch (error) {
if (attempt === maxRetries - 1) throw error;
}
}
throw new Error('Max retries exceeded');
}
Monitoring & Metrics
Key Metrics to Track
- Rate Limit Hit Rate: Percentage of requests hitting limits
- 429 Response Count: Total rate limit errors by endpoint
- Top Rate Limited IPs: Identify potential abuse patterns
- False Positive Rate: Legitimate users hitting limits
Alerting Thresholds
Warning Alerts:
- Rate limit hit rate > 5% on any endpoint
- Single IP hits rate limit > 10 times in 1 hour
Critical Alerts:
- Rate limit hit rate > 20% (may indicate DDoS)
- Multiple IPs hitting limits simultaneously (coordinated attack)
Rate Limit Adjustments
Increasing Limits for Legitimate Use
If you have a legitimate use case requiring higher limits:
- Contact Support: Describe your use case and expected volume
- Verification: We'll verify your account and usage patterns
- Temporary Increase: May grant temporary limit increase
- Custom Tier: High-volume verified accounts may get custom limits
Examples of Valid Requests:
- Bulk data migration project
- Integration with external service
- High-traffic public API client
Technical Implementation
Architecture
Rate limiting is implemented using in-memory rate limiting with:
- Storage: Map-based storage (IP → {count, resetAt})
- Cleanup: Periodic cleanup of expired entries (every 30 seconds)
- Capacity Management: LRU eviction when map exceeds 10,000 entries
- Emergency Handling: Automatic cleanup if memory pressure detected
Memory Management
Map Capacity: 10,000 unique IPs tracked simultaneously Cleanup Interval: Every 30 seconds or half the rate limit window LRU Eviction: Removes 30% oldest entries when at capacity
Shared Middleware
All edge functions use the shared rate limiter:
import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts';
const limiter = rateLimiters.strict; // or .standard, .lenient, .perUser(n)
serve(withRateLimit(async (req) => {
// Your edge function logic
}, limiter, corsHeaders));
Security Considerations
IP Spoofing Protection
Rate limiting uses X-Forwarded-For header (first IP in chain):
- Trusts proxy headers in production (Cloudflare, Supabase)
- Prevents IP spoofing by using first IP only
- Falls back to
X-Real-IPifX-Forwarded-Forunavailable
Distributed Attacks
Current Limitation: In-memory rate limiting is per-edge-function instance
- Distributed attacks across multiple instances may bypass limits
- Future: Consider distributed rate limiting (Redis, Supabase table)
Mitigation:
- Monitor aggregate request rates across all instances
- Use Cloudflare rate limiting as first line of defense
- Alert on unusual traffic patterns
Bypassing Rate Limits
Important: Rate limits CANNOT be bypassed, even for authenticated users.
Why No Bypass?:
- Prevents credential compromise from affecting system stability
- Ensures fair usage across all users
- Protects backend infrastructure
Moderator/Admin Considerations:
- Per-user rate limiting allows higher individual limits
- Moderators have different tiers for moderation actions
- No complete bypass to prevent abuse of compromised accounts
Testing Rate Limits
Manual Testing
# Test upload-image rate limit (5 req/min)
for i in {1..6}; do
curl -X POST https://api.thrillwiki.com/functions/v1/upload-image \
-H "Authorization: Bearer $TOKEN" \
-d '{}' && echo "Request $i succeeded"
done
# Expected: First 5 succeed, 6th returns 429
Automated Testing
describe('Rate Limiting', () => {
test('enforces strict limits on upload-image', async () => {
const requests = [];
// Make 6 requests (limit is 5)
for (let i = 0; i < 6; i++) {
requests.push(fetch('/upload-image', { method: 'POST' }));
}
const responses = await Promise.all(requests);
const statuses = responses.map(r => r.status);
expect(statuses.filter(s => s === 200).length).toBe(5);
expect(statuses.filter(s => s === 429).length).toBe(1);
});
});
Future Enhancements
Planned Improvements
- Database-Backed Rate Limiting: Persistent rate limiting across edge function instances
- Dynamic Rate Limits: Adjust limits based on system load
- User Reputation System: Higher limits for trusted users
- API Keys: Rate limiting by API key for integrations
- Cost-Based Limiting: Different limits for different operation costs
Related Documentation
Troubleshooting
"Rate limit exceeded" when I haven't made many requests
Possible Causes:
- Shared IP: You're behind a NAT/VPN sharing an IP with others
- Recent Requests: Rate limit window hasn't reset yet
- Multiple Tabs: Multiple browser tabs making requests
Solutions:
- Wait for rate limit window to reset (shown in
Retry-Afterheader) - Check browser dev tools for unexpected background requests
- Disable browser extensions that might be making requests
Rate limit seems inconsistent
Explanation: Rate limiting is per-edge-function instance
- Multiple instances may have separate rate limit counters
- Distributed traffic may see different limits
- This is expected behavior for in-memory rate limiting
Contact
For rate limit issues or increase requests:
- Support: [Contact form on ThrillWiki]
- Documentation: https://docs.thrillwiki.com
- Status: https://status.thrillwiki.com