# Rate Limiting & Throttling **Meta-Description:** API Rate Limiting implementieren. Token Bucket, Sliding Window und Redis-basierte Rate Limiter für sichere APIs. **Keywords:** Rate Limiting, API Throttling, Token Bucket, Sliding Window, Redis, API Security, DDoS Protection --- ## Einführung **Rate Limiting** schützt APIs vor Überlastung und Missbrauch. Von **Token Bucket** bis **Sliding Window** – verschiedene Algorithmen haben unterschiedliche Stärken. Dieser Guide zeigt Implementation mit Redis und Best Practices. --- ## Rate Limiting Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ RATE LIMITING ALGORITHMS │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. FIXED WINDOW: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ |----Window 1----|----Window 2----| │ │ │ │ |▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓|░░░░░░░░░░░░░░░| │ │ │ │ 100 requests/min Reset at boundary │ │ │ │ ✓ Simple │ │ │ │ ✗ Burst at window boundary │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 2. SLIDING WINDOW LOG: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Store timestamps of all requests │ │ │ │ [t1, t2, t3, ... tn] │ │ │ │ Count requests in last N seconds │ │ │ │ ✓ Accurate │ │ │ │ ✗ Memory intensive │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 3. SLIDING WINDOW COUNTER: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Previous window: 80 requests │ │ │ │ Current window: 20 requests │ │ │ │ Position: 25% into current window │ │ │ │ Weighted: 80 * 0.75 + 20 = 80 requests │ │ │ │ ✓ Memory efficient │ │ │ │ ✓ Smooth rate limiting │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 4. TOKEN BUCKET: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Bucket capacity: 100 tokens │ │ │ │ Refill rate: 10 tokens/second │ │ │ │ Request consumes 1 token │ │ │ │ ✓ Allows controlled bursts │ │ │ │ ✓ Smooth average rate │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 5. LEAKY BUCKET: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Requests enter bucket (queue) │ │ │ │ Processed at fixed rate (leak) │ │ │ │ Overflow rejected │ │ │ │ ✓ Smooth output rate │ │ │ │ ✗ Doesn't allow bursts │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Redis Rate Limiter ```typescript // lib/rate-limit/redis.ts import { Redis } from 'ioredis'; const redis = new Redis(process.env.REDIS_URL!); interface RateLimitConfig { identifier: string; limit: number; window: number; // seconds } interface RateLimitResult { allowed: boolean; remaining: number; reset: number; retryAfter?: number; } // Sliding Window Counter export async function slidingWindowRateLimit( config: RateLimitConfig ): Promise { const { identifier, limit, window } = config; const now = Date.now(); const windowStart = now - window * 1000; const currentWindow = Math.floor(now / (window * 1000)); const previousWindow = currentWindow - 1; const keys = { current: `ratelimit:${identifier}:${currentWindow}`, previous: `ratelimit:${identifier}:${previousWindow}` }; // Get counts from both windows const [currentCount, previousCount] = await redis.mget(keys.current, keys.previous); const current = parseInt(currentCount || '0'); const previous = parseInt(previousCount || '0'); // Calculate weighted count const windowPosition = (now % (window * 1000)) / (window * 1000); const weightedCount = Math.floor(previous * (1 - windowPosition) + current); if (weightedCount >= limit) { const reset = (currentWindow + 1) * window * 1000; return { allowed: false, remaining: 0, reset, retryAfter: Math.ceil((reset - now) / 1000) }; } // Increment current window await redis .multi() .incr(keys.current) .expire(keys.current, window * 2) .exec(); return { allowed: true, remaining: limit - weightedCount - 1, reset: (currentWindow + 1) * window * 1000 }; } // Token Bucket export async function tokenBucketRateLimit( identifier: string, bucketSize: number, refillRate: number // tokens per second ): Promise { const key = `ratelimit:bucket:${identifier}`; const now = Date.now(); // Lua script for atomic operation const luaScript = ` local key = KEYS[1] local bucket_size = tonumber(ARGV[1]) local refill_rate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') local tokens = tonumber(bucket[1]) or bucket_size local last_refill = tonumber(bucket[2]) or now -- Calculate tokens to add local elapsed = (now - last_refill) / 1000 local tokens_to_add = elapsed * refill_rate tokens = math.min(bucket_size, tokens + tokens_to_add) -- Try to consume a token if tokens >= 1 then tokens = tokens - 1 redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now) redis.call('EXPIRE', key, bucket_size / refill_rate * 2) return {1, tokens, 0} else local wait_time = (1 - tokens) / refill_rate return {0, tokens, wait_time} end `; const result = await redis.eval( luaScript, 1, key, bucketSize, refillRate, now ) as [number, number, number]; const [allowed, remaining, waitTime] = result; return { allowed: allowed === 1, remaining: Math.floor(remaining), reset: now + Math.ceil((bucketSize - remaining) / refillRate) * 1000, retryAfter: waitTime > 0 ? Math.ceil(waitTime) : undefined }; } ``` --- ## Rate Limit Middleware ```typescript // lib/rate-limit/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { slidingWindowRateLimit } from './redis'; interface RateLimitTier { identifier: (req: NextRequest) => string; limit: number; window: number; } const rateLimitTiers: Record = { // Public API - by IP public: { identifier: (req) => req.ip || req.headers.get('x-forwarded-for') || 'unknown', limit: 100, window: 60 // 100 requests per minute }, // Authenticated API - by user authenticated: { identifier: (req) => req.headers.get('x-user-id') || 'unknown', limit: 1000, window: 60 // 1000 requests per minute }, // Premium API - by API key premium: { identifier: (req) => req.headers.get('x-api-key') || 'unknown', limit: 10000, window: 60 // 10000 requests per minute } }; export async function rateLimitMiddleware( request: NextRequest, tier: keyof typeof rateLimitTiers = 'public' ): Promise { const config = rateLimitTiers[tier]; const identifier = config.identifier(request); const result = await slidingWindowRateLimit({ identifier: `${tier}:${identifier}`, limit: config.limit, window: config.window }); // Set rate limit headers const headers = new Headers(); headers.set('X-RateLimit-Limit', config.limit.toString()); headers.set('X-RateLimit-Remaining', result.remaining.toString()); headers.set('X-RateLimit-Reset', result.reset.toString()); if (!result.allowed) { headers.set('Retry-After', result.retryAfter!.toString()); return NextResponse.json( { error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests', retryAfter: result.retryAfter } }, { status: 429, headers } ); } return null; // Continue to handler } // Usage in API route // app/api/[...]/route.ts export async function GET(request: NextRequest) { const rateLimitResponse = await rateLimitMiddleware(request, 'authenticated'); if (rateLimitResponse) return rateLimitResponse; // Handle request... } ``` --- ## Plan-Based Rate Limits ```typescript // lib/rate-limit/plans.ts import { db } from '@/lib/db'; import { slidingWindowRateLimit } from './redis'; interface PlanLimits { requestsPerMinute: number; requestsPerDay: number; burstSize: number; } const planLimits: Record = { free: { requestsPerMinute: 60, requestsPerDay: 1000, burstSize: 10 }, pro: { requestsPerMinute: 600, requestsPerDay: 50000, burstSize: 100 }, enterprise: { requestsPerMinute: 6000, requestsPerDay: 1000000, burstSize: 1000 } }; export async function checkPlanRateLimit( userId: string, endpoint: string ): Promise<{ allowed: boolean; error?: string; headers: Record }> { // Get user's plan const user = await db.user.findUnique({ where: { id: userId }, select: { plan: true } }); const plan = user?.plan || 'free'; const limits = planLimits[plan]; // Check per-minute limit const minuteResult = await slidingWindowRateLimit({ identifier: `user:${userId}:minute`, limit: limits.requestsPerMinute, window: 60 }); // Check daily limit const dailyResult = await slidingWindowRateLimit({ identifier: `user:${userId}:day`, limit: limits.requestsPerDay, window: 86400 }); const headers = { 'X-RateLimit-Limit-Minute': limits.requestsPerMinute.toString(), 'X-RateLimit-Remaining-Minute': minuteResult.remaining.toString(), 'X-RateLimit-Limit-Day': limits.requestsPerDay.toString(), 'X-RateLimit-Remaining-Day': dailyResult.remaining.toString() }; if (!minuteResult.allowed) { return { allowed: false, error: 'Rate limit exceeded (per minute)', headers: { ...headers, 'Retry-After': minuteResult.retryAfter!.toString() } }; } if (!dailyResult.allowed) { return { allowed: false, error: 'Daily rate limit exceeded. Please upgrade your plan.', headers: { ...headers, 'Retry-After': dailyResult.retryAfter!.toString() } }; } return { allowed: true, headers }; } ``` --- ## Endpoint-Specific Limits ```typescript // lib/rate-limit/endpoint.ts interface EndpointRateLimit { path: string | RegExp; method?: string; limit: number; window: number; keyGenerator?: (req: Request) => string; } const endpointLimits: EndpointRateLimit[] = [ // Strict limit for auth endpoints { path: '/api/auth/login', method: 'POST', limit: 5, window: 300, // 5 attempts per 5 minutes keyGenerator: (req) => req.headers.get('x-forwarded-for') || 'unknown' }, // Password reset { path: '/api/auth/reset-password', method: 'POST', limit: 3, window: 3600 // 3 per hour }, // File uploads { path: /^\/api\/files\/upload/, limit: 10, window: 60 // 10 per minute }, // Search (expensive) { path: '/api/search', limit: 30, window: 60 }, // AI/LLM endpoints { path: /^\/api\/ai\//, limit: 10, window: 60 } ]; export async function getEndpointRateLimit( path: string, method: string ): Promise { return endpointLimits.find(limit => { const pathMatch = typeof limit.path === 'string' ? path === limit.path : limit.path.test(path); const methodMatch = !limit.method || limit.method === method; return pathMatch && methodMatch; }) || null; } ``` --- ## Distributed Rate Limiting ```typescript // lib/rate-limit/distributed.ts import { Redis } from 'ioredis'; // Cluster-aware rate limiting export class DistributedRateLimiter { private redis: Redis; private localCounter: Map = new Map(); private syncInterval: number = 1000; // 1 second constructor(redisUrl: string) { this.redis = new Redis(redisUrl); this.startSync(); } private startSync() { setInterval(async () => { // Sync local counters to Redis const pipeline = this.redis.pipeline(); this.localCounter.forEach((count, key) => { if (count > 0) { pipeline.incrby(key, count); } }); await pipeline.exec(); this.localCounter.clear(); }, this.syncInterval); } async checkLimit( key: string, limit: number, window: number ): Promise { // Quick local check (approximate) const localCount = this.localCounter.get(key) || 0; // Get current count from Redis const redisCount = parseInt(await this.redis.get(key) || '0'); const totalCount = redisCount + localCount; if (totalCount >= limit) { return false; } // Increment locally this.localCounter.set(key, localCount + 1); // Set expiry if new key if (redisCount === 0) { await this.redis.setex(key, window, '0'); } return true; } } ``` --- ## Rate Limit Dashboard ```tsx // components/RateLimitStatus.tsx 'use client'; import { useState, useEffect } from 'react'; interface RateLimitStatus { endpoint: string; limit: number; used: number; remaining: number; resetsAt: Date; } export function RateLimitStatus({ apiKey }: { apiKey: string }) { const [status, setStatus] = useState([]); useEffect(() => { fetch(`/api/rate-limits/status?apiKey=${apiKey}`) .then(res => res.json()) .then(setStatus); }, [apiKey]); return (

Rate Limit Status

{status.map((item) => (
{item.endpoint} {item.remaining} / {item.limit} remaining

Resets: {new Date(item.resetsAt).toLocaleTimeString()}

))}
); } ``` --- ## Best Practices | Aspect | Recommendation | |--------|----------------| | **Algorithm** | Sliding window for accuracy | | **Storage** | Redis for distributed systems | | **Headers** | Always return limit headers | | **Granularity** | Per-user + per-IP | | **Grace Period** | Soft limits before hard block | | **Documentation** | Clear rate limit docs | --- ## Fazit Rate Limiting erfordert: 1. **Algorithmus**: Sliding Window oder Token Bucket 2. **Skalierung**: Redis für verteilte Systeme 3. **Granularität**: User, IP, Endpoint-spezifisch 4. **Transparenz**: Headers und Dokumentation Rate Limiting schützt APIs und verbessert Fairness. --- ## Bildprompts 1. "Rate limiting algorithm comparison diagram, token bucket and sliding window" 2. "API rate limit dashboard, usage gauges and quotas" 3. "Distributed rate limiting architecture, Redis cluster" --- ## Quellen - [Stripe Rate Limiting](https://stripe.com/blog/rate-limiters) - [Cloudflare Rate Limiting](https://blog.cloudflare.com/counting-things-a-lot-of-different-things/) - [Redis Rate Limiting Patterns](https://redis.io/commands/incr#pattern-rate-limiter) - [Kong Rate Limiting](https://docs.konghq.com/hub/kong-inc/rate-limiting/)