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

  1. Identifier: Rate limits are tracked by IP address
  2. Storage: In-memory Map (cleared on Worker restart)
  3. Window: Sliding window per limit type
  4. 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/vote
  • POST /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 requests
  • X-RateLimit-Remaining: 0
  • X-RateLimit-Reset: Reset timestamp

IP Address Detection

The system detects IP addresses in this order:

  1. CF-Connecting-IP (Cloudflare header)
  2. X-Forwarded-For (fallback)
  3. "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

  1. In-Memory: Lost on Worker restart
  2. Per-Worker: Not shared across Workers
  3. No Persistence: No long-term tracking
  4. 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:

  1. Cloudflare Dashboard → Security → Rate Limiting
  2. Create rule for /e/*/vote
  3. 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 RateLimiter Durable 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

  1. Respect Rate Limits: Check X-RateLimit-Remaining header
  2. Handle 429 Errors: Implement exponential backoff
  3. Cache Responses: Reduce API calls
  4. Batch Operations: Combine multiple requests when possible

For Administrators

  1. Monitor Rate Limits: Watch for excessive 429 responses
  2. Adjust Limits: Tune based on usage patterns
  3. Upgrade When Needed: Move to distributed system for scale
  4. Set Appropriate Limits: Balance security and usability

Monitoring

Metrics to Track

  1. 429 Response Rate: Should be < 1%
  2. Rate Limit Hits: Track which IPs hit limits
  3. Average Remaining: Should be > 50% for most users
  4. 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:

  1. Too many requests in short time
  2. Multiple users behind same IP (NAT)
  3. Bot/crawler making requests

Solutions:

  1. Wait for rate limit window to reset
  2. Check X-RateLimit-Reset header
  3. Implement exponential backoff
  4. Contact admin if legitimate use case

False Positives

Scenario: Legitimate users behind NAT hitting limits

Solutions:

  1. Increase rate limits
  2. Use Cloudflare Rate Limiting (more sophisticated)
  3. 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

  1. Add to RATE_LIMITS:
export const RATE_LIMITS = {
  // ... existing
  custom: { windowMs: 60 * 1000, maxRequests: 20 }
};
  1. Apply middleware:
route.post("/endpoint", rateLimitMiddleware("custom"), handler);

See Also