|$ curl https://forge-ai.dev/api/markdown?path=docs/security/rate-limiting
$cat docs/rate-limiting.md
updated Recently·30 min read·published

Rate Limiting

SecurityIntermediate🎯Free Tools
Introduction

Rate limiting controls how many requests a client can make within a time window. It protects APIs from abuse, DDoS attacks, and accidental overload. Without rate limiting, a single misbehaving client can take down your entire service.

Rate Limiting Algorithms
AlgorithmHow It WorksPros
Fixed WindowCount requests in fixed time windowsSimple, low memory
Sliding WindowWeighted average of current + previous windowSmooths boundary spikes
Token BucketTokens refill at steady rate; each request costs a tokenAllows controlled bursts
Leaky BucketRequests queue; processed at fixed ratePerfectly smooth output
token-bucket.ts
TypeScript
1// Token Bucket algorithm implementation
2class TokenBucket {
3 private tokens: number;
4 private lastRefill: number;
5
6 constructor(
7 private readonly capacity: number,
8 private readonly refillRate: number, // tokens per second
9 ) {
10 this.tokens = capacity;
11 this.lastRefill = Date.now();
12 }
13
14 consume(tokens: number = 1): boolean {
15 this.refill();
16 if (this.tokens >= tokens) {
17 this.tokens -= tokens;
18 return true;
19 }
20 return false;
21 }
22
23 private refill() {
24 const now = Date.now();
25 const elapsed = (now - this.lastRefill) / 1000;
26 this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
27 this.lastRefill = now;
28 }
29}
30
31// Usage: 100 requests per minute, burst of 10
32const bucket = new TokenBucket(10, 100 / 60);
33
34function handleRequest(req: Request) {
35 if (!bucket.consume()) {
36 return new Response("Rate limit exceeded", {
37 status: 429,
38 headers: {
39 "Retry-After": "60",
40 "X-RateLimit-Limit": "100",
41 "X-RateLimit-Remaining": "0",
42 },
43 });
44 }
45 // Process request...
46}
Redis-Based Rate Limiting
redis-ratelimit.ts
TypeScript
1// Redis sliding window rate limiter
2import Redis from "ioredis";
3
4const redis = new Redis(process.env.REDIS_URL);
5
6async function isRateLimited(
7 key: string,
8 limit: number,
9 windowMs: number,
10): Promise<{ allowed: boolean; remaining: number }> {
11 const now = Date.now();
12 const windowStart = now - windowMs;
13 const multi = redis.multi();
14
15 // Remove old entries
16 multi.zremrangebyscore(key, 0, windowStart);
17 // Add current request
18 multi.zadd(key, now, `${now}-${Math.random()}`);
19 // Count requests in window
20 multi.zcard(key);
21 // Set expiry
22 multi.pexpire(key, windowMs);
23
24 const results = await multi.exec();
25 const count = results![2][1] as number;
26
27 return {
28 allowed: count <= limit,
29 remaining: Math.max(0, limit - count),
30 };
31}
32
33// Next.js API route middleware
34export async function rateLimit(request: Request, limit = 100, windowMs = 60000) {
35 const ip = request.headers.get("x-forwarded-for") || "unknown";
36 const key = `ratelimit:${ip}`;
37
38 const { allowed, remaining } = await isRateLimited(key, limit, windowMs);
39
40 if (!allowed) {
41 return new Response(JSON.stringify({ error: "Rate limit exceeded" }), {
42 status: 429,
43 headers: {
44 "Content-Type": "application/json",
45 "X-RateLimit-Limit": String(limit),
46 "X-RateLimit-Remaining": "0",
47 "Retry-After": String(Math.ceil(windowMs / 1000)),
48 },
49 });
50 }
51 return null; // Allowed
52}

info

Use Redis MULTI (transactions) to ensure atomicity. Without it, race conditions can cause incorrect rate limit counts under high concurrency.
Per-User & Tiered Limits
per-user-limits.ts
TypeScript
1// Different rate limits per user tier
2interface RateLimitConfig {
3 requests: number;
4 windowMs: number;
5}
6
7const TIER_LIMITS: Record<string, RateLimitConfig> = {
8 free: { requests: 60, windowMs: 60_000 }, // 60/min
9 pro: { requests: 600, windowMs: 60_000 }, // 600/min
10 enterprise: { requests: 6000, windowMs: 60_000 }, // 6000/min
11};
12
13async function rateLimitForUser(request: Request) {
14 const user = await authenticateUser(request);
15 const tier = user?.tier || "anonymous";
16 const config = tier === "anonymous"
17 ? { requests: 20, windowMs: 60_000 }
18 : TIER_LIMITS[tier];
19
20 const key = `ratelimit:${tier}:${user?.id || getIP(request)}`;
21 return isRateLimited(key, config.requests, config.windowMs);
22}
23
24// Endpoint-specific limits
25const ENDPOINT_LIMITS: Record<string, number> = {
26 "/api/auth/login": 5, // 5 login attempts per minute
27 "/api/search": 30, // 30 searches per minute
28 "/api/upload": 10, // 10 uploads per minute
29 "/api/export": 3, // 3 exports per minute
30};
Rate Limit Headers
headers.ts
TypeScript
1// Standard rate limit response headers
2// X-RateLimit-Limit: Maximum requests per window
3// X-RateLimit-Remaining: Requests remaining
4// X-RateLimit-Reset: Unix timestamp when window resets
5// Retry-After: Seconds to wait (on 429)
6
7function rateLimitHeaders(limit: number, remaining: number, resetMs: number) {
8 return {
9 "X-RateLimit-Limit": String(limit),
10 "X-RateLimit-Remaining": String(remaining),
11 "X-RateLimit-Reset": String(Math.ceil(resetMs / 1000)),
12 };
13}
14
15// RFC 6585 compliant 429 response
16function tooManyRequests(retryAfter: number) {
17 return new Response(
18 JSON.stringify({ error: "Too Many Requests", retryAfter }),
19 {
20 status: 429,
21 headers: {
22 "Content-Type": "application/json",
23 "Retry-After": String(retryAfter),
24 ...rateLimitHeaders(100, 0, Date.now() + retryAfter * 1000),
25 },
26 }
27 );
28}
$Blueprint — Engineering Documentation·Section ID: SEC-RL-01·Revision: 1.0