Overview
Rate limiting protects the voting system from abuse, spam, and denial-of-service attacks by limiting the number of requests per time window from each IP address.
Implementation
Current System
The system uses in-memory rate limiting implemented in src/utils/rate-limit.ts. This is suitable for MVP and moderate traffic, but consider upgrading for high-scale production.
How It Works
- Identifier: Rate limits are tracked by IP address
- Storage: In-memory Map (cleared on Worker restart)
- Window: Sliding window per limit type
- Cleanup: Automatic cleanup of expired entries (1% chance per request)
Rate Limit Store
interface RateLimitEntry {
count: number; // Requests in current window
resetAt: number; // Timestamp when window resets
}
const rateLimitStore = new Map<string, RateLimitEntry>();Key Format: rate_limit:${limitType}:${ipAddress}
Rate Limit Configurations
General API
Limit: 100 requests per minute
Applied To:
- General API endpoints
- Election viewing
- Results viewing
Configuration:
general: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 100
}Vote Submission
Limit: 5 requests per minute
Applied To:
- Vote submission endpoints
POST /e/:id/votePOST /e/:id/vote/poll
Configuration:
vote: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 5
}Rationale: Prevents vote spam and abuse while allowing legitimate retries.
Token Validation
Limit: 10 requests per minute
Applied To:
- Token validation attempts
- Invalid token checks
Configuration:
tokenValidation: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 10
}Rationale: Prevents brute-force token guessing.
Admin
Limit: 50 requests per minute
Applied To:
- All
/admin/*endpoints - Admin API calls
Configuration:
admin: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 50
}Middleware
Usage
Rate limiting is applied via middleware:
import { rateLimitMiddleware } from "../middleware/rate-limit";
// Apply to route
vote.post("/:id/vote", rateLimitMiddleware("vote"), async (c) => {
// Handler
});Middleware Function
export function rateLimitMiddleware(limitType: keyof typeof RATE_LIMITS) {
return async (c: Context, next: Next) => {
const ip = c.req.header("CF-Connecting-IP") ||
c.req.header("X-Forwarded-For") ||
"unknown";
const identifier = `rate_limit:${limitType}:${ip}`;
const limit = checkRateLimit(identifier, RATE_LIMITS[limitType]);
if (!limit.allowed) {
return c.json({
error: "Rate limit exceeded",
retryAfter: Math.ceil((limit.resetAt - Date.now()) / 1000)
}, 429);
}
// Add headers
c.header("X-RateLimit-Limit", RATE_LIMITS[limitType].maxRequests.toString());
c.header("X-RateLimit-Remaining", limit.remaining.toString());
c.header("X-RateLimit-Reset", limit.resetAt.toString());
await next();
};
}Response Headers
All rate-limited endpoints include headers:
X-RateLimit-Limit
Maximum requests allowed in the window.
Example: X-RateLimit-Limit: 5
X-RateLimit-Remaining
Number of requests remaining in the current window.
Example: X-RateLimit-Remaining: 3
X-RateLimit-Reset
Timestamp (milliseconds) when the rate limit window resets.
Example: X-RateLimit-Reset: 1704067200000
Error Response
When rate limit is exceeded:
Status Code: 429 Too Many Requests
Response Body:
{
"error": "Rate limit exceeded",
"retryAfter": 45
}Headers:
X-RateLimit-Limit: Maximum requestsX-RateLimit-Remaining: 0X-RateLimit-Reset: Reset timestamp
IP Address Detection
The system detects IP addresses in this order:
CF-Connecting-IP(Cloudflare header)X-Forwarded-For(fallback)"unknown"(if neither available)
Cloudflare IP Detection
Cloudflare automatically sets CF-Connecting-IP with the real client IP, even when behind proxies.
Cleanup Strategy
Automatic Cleanup
The system automatically cleans up expired entries:
if (Math.random() < 0.01) { // 1% chance
for (const [key, value] of rateLimitStore.entries()) {
if (now > value.resetAt) {
rateLimitStore.delete(key);
}
}
}Why 1% chance?
- Reduces overhead
- Cleanup happens frequently enough
- Map size stays manageable
Manual Cleanup
Not needed - automatic cleanup is sufficient.
Limitations
Current Implementation
- In-Memory: Lost on Worker restart
- Per-Worker: Not shared across Workers
- No Persistence: No long-term tracking
- Simple Algorithm: Sliding window, not token bucket
Impact
- Cold Starts: Rate limits reset on Worker restart
- Multiple Workers: Each Worker has separate limits
- No Long-Term Tracking: Can’t track abuse over days
When to Upgrade
Consider upgrading if:
- High traffic (>1000 requests/second)
- Need persistent rate limiting
- Need distributed rate limiting
- Need more sophisticated algorithms
Upgrade Options
1. Cloudflare Rate Limiting
Pros:
- Built into Cloudflare
- Distributed (works across Workers)
- Persistent
- Configurable via dashboard
Cons:
- Requires paid plan
- Less control over algorithm
Setup:
- Cloudflare Dashboard → Security → Rate Limiting
- Create rule for
/e/*/vote - Set limit (e.g., 5 per minute)
2. Durable Objects
Pros:
- Full control
- Persistent storage
- Can implement token bucket
- Works across Workers
Cons:
- More complex
- Additional latency (~1-2ms)
Implementation:
- Create
RateLimiterDurable Object - Store rate limit state per IP
- Use same ID format:
rate_limit:${type}:${ip}
3. D1 Database
Pros:
- Persistent
- Simple to implement
- Works across Workers
Cons:
- Higher latency
- Requires cleanup job
- Not ideal for high frequency
Implementation:
- Store rate limit entries in D1
- Cleanup expired entries via cron
- Query on each request
Best Practices
For Developers
- Respect Rate Limits: Check
X-RateLimit-Remainingheader - Handle 429 Errors: Implement exponential backoff
- Cache Responses: Reduce API calls
- Batch Operations: Combine multiple requests when possible
For Administrators
- Monitor Rate Limits: Watch for excessive 429 responses
- Adjust Limits: Tune based on usage patterns
- Upgrade When Needed: Move to distributed system for scale
- Set Appropriate Limits: Balance security and usability
Monitoring
Metrics to Track
- 429 Response Rate: Should be < 1%
- Rate Limit Hits: Track which IPs hit limits
- Average Remaining: Should be > 50% for most users
- Reset Times: Track when limits reset
Logging
Rate limit violations are logged:
- Action:
rate_limit.exceeded - Metadata: IP address, limit type, endpoint
- See audit-logging
Troubleshooting
”Rate limit exceeded” Error
Possible causes:
- Too many requests in short time
- Multiple users behind same IP (NAT)
- Bot/crawler making requests
Solutions:
- Wait for rate limit window to reset
- Check
X-RateLimit-Resetheader - Implement exponential backoff
- Contact admin if legitimate use case
False Positives
Scenario: Legitimate users behind NAT hitting limits
Solutions:
- Increase rate limits
- Use Cloudflare Rate Limiting (more sophisticated)
- Implement per-user rate limiting (requires authentication)
Configuration
Changing Limits
Edit src/utils/rate-limit.ts:
export const RATE_LIMITS = {
general: { windowMs: 60 * 1000, maxRequests: 100 },
vote: { windowMs: 60 * 1000, maxRequests: 5 },
// ... adjust as needed
};Adding New Limit Types
- Add to
RATE_LIMITS:
export const RATE_LIMITS = {
// ... existing
custom: { windowMs: 60 * 1000, maxRequests: 20 }
};- Apply middleware:
route.post("/endpoint", rateLimitMiddleware("custom"), handler);See Also
- authentication - Admin authentication
- voting-system - Vote submission
- api-reference - API endpoint documentation