rate-limiting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rate Limiting

速率限制

Protect your API with subscription-tier aware rate limiting.
使用支持订阅层级的速率限制保护你的API。

When to Use This Skill

适用场景

  • Building a SaaS API with different subscription tiers
  • Protecting endpoints from abuse
  • Implementing fair usage policies
  • Adding rate limits to existing APIs
  • 构建带有不同订阅层级的SaaS API
  • 保护接口免受滥用
  • 实施公平使用策略
  • 为现有API添加速率限制

Core Concepts

核心概念

Sliding Window Algorithm

滑动窗口算法

More accurate than fixed windows, prevents burst at window boundaries:
Window: [----older----][----current----]
Weight:      30%             70%
比固定窗口更精准,可避免窗口边界处的突发流量:
Window: [----older----][----current----]
Weight:      30%             70%

Tier-Based Limits

基于层级的限制

TierRequests/minBurst
Free6010
Pro600100
Enterprise60001000
层级请求数/分钟突发流量
免费版6010
专业版600100
企业版60001000

TypeScript Implementation

TypeScript 实现

typescript
// rate-limiter.ts
import { Redis } from 'ioredis';

interface RateLimitConfig {
  windowMs: number;
  maxRequests: number;
  burstLimit?: number;
}

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: number;
  retryAfter?: number;
}

const TIER_LIMITS: Record<string, RateLimitConfig> = {
  free: { windowMs: 60000, maxRequests: 60, burstLimit: 10 },
  pro: { windowMs: 60000, maxRequests: 600, burstLimit: 100 },
  enterprise: { windowMs: 60000, maxRequests: 6000, burstLimit: 1000 },
};

class RateLimiter {
  constructor(private redis: Redis) {}

  async check(key: string, tier: string = 'free'): Promise<RateLimitResult> {
    const config = TIER_LIMITS[tier] || TIER_LIMITS.free;
    const now = Date.now();
    const windowStart = now - config.windowMs;

    const multi = this.redis.multi();
    
    // Remove old entries
    multi.zremrangebyscore(key, 0, windowStart);
    // Count current window
    multi.zcard(key);
    // Add current request
    multi.zadd(key, now.toString(), `${now}-${Math.random()}`);
    // Set expiry
    multi.expire(key, Math.ceil(config.windowMs / 1000) + 1);

    const results = await multi.exec();
    const currentCount = (results?.[1]?.[1] as number) || 0;

    const allowed = currentCount < config.maxRequests;
    const remaining = Math.max(0, config.maxRequests - currentCount - 1);
    const resetAt = now + config.windowMs;

    return {
      allowed,
      remaining,
      resetAt,
      retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
    };
  }
}

export { RateLimiter, RateLimitConfig, RateLimitResult, TIER_LIMITS };
typescript
// rate-limiter.ts
import { Redis } from 'ioredis';

interface RateLimitConfig {
  windowMs: number;
  maxRequests: number;
  burstLimit?: number;
}

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: number;
  retryAfter?: number;
}

const TIER_LIMITS: Record<string, RateLimitConfig> = {
  free: { windowMs: 60000, maxRequests: 60, burstLimit: 10 },
  pro: { windowMs: 60000, maxRequests: 600, burstLimit: 100 },
  enterprise: { windowMs: 60000, maxRequests: 6000, burstLimit: 1000 },
};

class RateLimiter {
  constructor(private redis: Redis) {}

  async check(key: string, tier: string = 'free'): Promise<RateLimitResult> {
    const config = TIER_LIMITS[tier] || TIER_LIMITS.free;
    const now = Date.now();
    const windowStart = now - config.windowMs;

    const multi = this.redis.multi();
    
    // 移除旧记录
    multi.zremrangebyscore(key, 0, windowStart);
    // 统计当前窗口内的请求数
    multi.zcard(key);
    // 添加当前请求记录
    multi.zadd(key, now.toString(), `${now}-${Math.random()}`);
    // 设置过期时间
    multi.expire(key, Math.ceil(config.windowMs / 1000) + 1);

    const results = await multi.exec();
    const currentCount = (results?.[1]?.[1] as number) || 0;

    const allowed = currentCount < config.maxRequests;
    const remaining = Math.max(0, config.maxRequests - currentCount - 1);
    const resetAt = now + config.windowMs;

    return {
      allowed,
      remaining,
      resetAt,
      retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
    };
  }
}

export { RateLimiter, RateLimitConfig, RateLimitResult, TIER_LIMITS };

Express Middleware

Express 中间件

typescript
// rate-limit-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { RateLimiter } from './rate-limiter';

interface RateLimitOptions {
  keyGenerator?: (req: Request) => string;
  tierResolver?: (req: Request) => string;
  skip?: (req: Request) => boolean;
}

function createRateLimitMiddleware(
  limiter: RateLimiter,
  options: RateLimitOptions = {}
) {
  const {
    keyGenerator = (req) => req.ip || 'unknown',
    tierResolver = (req) => (req as any).user?.tier || 'free',
    skip = () => false,
  } = options;

  return async (req: Request, res: Response, next: NextFunction) => {
    if (skip(req)) return next();

    const key = `ratelimit:${keyGenerator(req)}`;
    const tier = tierResolver(req);
    const result = await limiter.check(key, tier);

    // Set rate limit headers
    res.set({
      'X-RateLimit-Limit': TIER_LIMITS[tier]?.maxRequests || 60,
      'X-RateLimit-Remaining': result.remaining,
      'X-RateLimit-Reset': Math.ceil(result.resetAt / 1000),
    });

    if (!result.allowed) {
      res.set('Retry-After', result.retryAfter?.toString() || '60');
      return res.status(429).json({
        error: 'Too Many Requests',
        message: `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`,
        retryAfter: result.retryAfter,
      });
    }

    next();
  };
}

export { createRateLimitMiddleware };
typescript
// rate-limit-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { RateLimiter } from './rate-limiter';

interface RateLimitOptions {
  keyGenerator?: (req: Request) => string;
  tierResolver?: (req: Request) => string;
  skip?: (req: Request) => boolean;
}

function createRateLimitMiddleware(
  limiter: RateLimiter,
  options: RateLimitOptions = {}
) {
  const {
    keyGenerator = (req) => req.ip || 'unknown',
    tierResolver = (req) => (req as any).user?.tier || 'free',
    skip = () => false,
  } = options;

  return async (req: Request, res: Response, next: NextFunction) => {
    if (skip(req)) return next();

    const key = `ratelimit:${keyGenerator(req)}`;
    const tier = tierResolver(req);
    const result = await limiter.check(key, tier);

    // 设置速率限制响应头
    res.set({
      'X-RateLimit-Limit': TIER_LIMITS[tier]?.maxRequests || 60,
      'X-RateLimit-Remaining': result.remaining,
      'X-RateLimit-Reset': Math.ceil(result.resetAt / 1000),
    });

    if (!result.allowed) {
      res.set('Retry-After', result.retryAfter?.toString() || '60');
      return res.status(429).json({
        error: '请求过于频繁',
        message: `已超出速率限制,请在${result.retryAfter}秒后重试。`,
        retryAfter: result.retryAfter,
      });
    }

    next();
  };
}

export { createRateLimitMiddleware };

Python Implementation

Python 实现

python
undefined
python
undefined

rate_limiter.py

rate_limiter.py

import time from typing import Optional, NamedTuple from redis import Redis
class RateLimitResult(NamedTuple): allowed: bool remaining: int reset_at: float retry_after: Optional[int] = None
TIER_LIMITS = { "free": {"window_ms": 60000, "max_requests": 60, "burst_limit": 10}, "pro": {"window_ms": 60000, "max_requests": 600, "burst_limit": 100}, "enterprise": {"window_ms": 60000, "max_requests": 6000, "burst_limit": 1000}, }
class RateLimiter: def init(self, redis: Redis): self.redis = redis
def check(self, key: str, tier: str = "free") -> RateLimitResult:
    config = TIER_LIMITS.get(tier, TIER_LIMITS["free"])
    now = time.time() * 1000
    window_start = now - config["window_ms"]

    pipe = self.redis.pipeline()
    pipe.zremrangebyscore(key, 0, window_start)
    pipe.zcard(key)
    pipe.zadd(key, {f"{now}-{id(object())}": now})
    pipe.expire(key, int(config["window_ms"] / 1000) + 1)
    
    results = pipe.execute()
    current_count = results[1]

    allowed = current_count < config["max_requests"]
    remaining = max(0, config["max_requests"] - current_count - 1)
    reset_at = now + config["window_ms"]

    return RateLimitResult(
        allowed=allowed,
        remaining=remaining,
        reset_at=reset_at,
        retry_after=None if allowed else int(config["window_ms"] / 1000),
    )
undefined
import time from typing import Optional, NamedTuple from redis import Redis
class RateLimitResult(NamedTuple): allowed: bool remaining: int reset_at: float retry_after: Optional[int] = None
TIER_LIMITS = { "free": {"window_ms": 60000, "max_requests": 60, "burst_limit": 10}, "pro": {"window_ms": 60000, "max_requests": 600, "burst_limit": 100}, "enterprise": {"window_ms": 60000, "max_requests": 6000, "burst_limit": 1000}, }
class RateLimiter: def init(self, redis: Redis): self.redis = redis
def check(self, key: str, tier: str = "free") -> RateLimitResult:
    config = TIER_LIMITS.get(tier, TIER_LIMITS["free"])
    now = time.time() * 1000
    window_start = now - config["window_ms"]

    pipe = self.redis.pipeline()
    pipe.zremrangebyscore(key, 0, window_start)
    pipe.zcard(key)
    pipe.zadd(key, {f"{now}-{id(object())}": now})
    pipe.expire(key, int(config["window_ms"] / 1000) + 1)
    
    results = pipe.execute()
    current_count = results[1]

    allowed = current_count < config["max_requests"]
    remaining = max(0, config["max_requests"] - current_count - 1)
    reset_at = now + config["window_ms"]

    return RateLimitResult(
        allowed=allowed,
        remaining=remaining,
        reset_at=reset_at,
        retry_after=None if allowed else int(config["window_ms"] / 1000),
    )
undefined

FastAPI Middleware

FastAPI 中间件

python
undefined
python
undefined

fastapi_middleware.py

fastapi_middleware.py

from fastapi import Request, HTTPException from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware
class RateLimitMiddleware(BaseHTTPMiddleware): def init(self, app, limiter: RateLimiter): super().init(app) self.limiter = limiter
async def dispatch(self, request: Request, call_next):
    # Get user tier from request (customize based on your auth)
    tier = getattr(request.state, "user_tier", "free")
    key = f"ratelimit:{request.client.host}"
    
    result = self.limiter.check(key, tier)
    
    if not result.allowed:
        return JSONResponse(
            status_code=429,
            content={
                "error": "Too Many Requests",
                "retry_after": result.retry_after,
            },
            headers={
                "Retry-After": str(result.retry_after),
                "X-RateLimit-Remaining": "0",
            },
        )
    
    response = await call_next(request)
    response.headers["X-RateLimit-Remaining"] = str(result.remaining)
    response.headers["X-RateLimit-Reset"] = str(int(result.reset_at / 1000))
    return response
undefined
from fastapi import Request, HTTPException from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware
class RateLimitMiddleware(BaseHTTPMiddleware): def init(self, app, limiter: RateLimiter): super().init(app) self.limiter = limiter
async def dispatch(self, request: Request, call_next):
    # 从请求中获取用户层级(根据你的认证逻辑自定义)
    tier = getattr(request.state, "user_tier", "free")
    key = f"ratelimit:{request.client.host}"
    
    result = self.limiter.check(key, tier)
    
    if not result.allowed:
        return JSONResponse(
            status_code=429,
            content={
                "error": "请求过于频繁",
                "retry_after": result.retry_after,
            },
            headers={
                "Retry-After": str(result.retry_after),
                "X-RateLimit-Remaining": "0",
            },
        )
    
    response = await call_next(request)
    response.headers["X-RateLimit-Remaining"] = str(result.remaining)
    response.headers["X-RateLimit-Reset"] = str(int(result.reset_at / 1000))
    return response
undefined

In-Memory Alternative (No Redis)

内存存储替代方案(无需Redis)

typescript
// memory-rate-limiter.ts
class InMemoryRateLimiter {
  private windows = new Map<string, number[]>();

  check(key: string, tier: string = 'free'): RateLimitResult {
    const config = TIER_LIMITS[tier] || TIER_LIMITS.free;
    const now = Date.now();
    const windowStart = now - config.windowMs;

    // Get or create window
    let timestamps = this.windows.get(key) || [];
    
    // Remove old entries
    timestamps = timestamps.filter(t => t > windowStart);
    
    const allowed = timestamps.length < config.maxRequests;
    
    if (allowed) {
      timestamps.push(now);
      this.windows.set(key, timestamps);
    }

    return {
      allowed,
      remaining: Math.max(0, config.maxRequests - timestamps.length),
      resetAt: now + config.windowMs,
      retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
    };
  }

  // Cleanup old entries periodically
  cleanup(): void {
    const now = Date.now();
    for (const [key, timestamps] of this.windows) {
      const filtered = timestamps.filter(t => t > now - 60000);
      if (filtered.length === 0) {
        this.windows.delete(key);
      } else {
        this.windows.set(key, filtered);
      }
    }
  }
}
typescript
// memory-rate-limiter.ts
class InMemoryRateLimiter {
  private windows = new Map<string, number[]>();

  check(key: string, tier: string = 'free'): RateLimitResult {
    const config = TIER_LIMITS[tier] || TIER_LIMITS.free;
    const now = Date.now();
    const windowStart = now - config.windowMs;

    // 获取或创建请求窗口
    let timestamps = this.windows.get(key) || [];
    
    // 移除旧记录
    timestamps = timestamps.filter(t => t > windowStart);
    
    const allowed = timestamps.length < config.maxRequests;
    
    if (allowed) {
      timestamps.push(now);
      this.windows.set(key, timestamps);
    }

    return {
      allowed,
      remaining: Math.max(0, config.maxRequests - timestamps.length),
      resetAt: now + config.windowMs,
      retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
    };
  }

  // 定期清理旧记录
  cleanup(): void {
    const now = Date.now();
    for (const [key, timestamps] of this.windows) {
      const filtered = timestamps.filter(t => t > now - 60000);
      if (filtered.length === 0) {
        this.windows.delete(key);
      } else {
        this.windows.set(key, filtered);
      }
    }
  }
}

Best Practices

最佳实践

  1. Use Redis for distributed systems: In-memory only works for single instances
  2. Set appropriate headers: Clients need
    X-RateLimit-*
    and
    Retry-After
  3. Return 429 status: Standard HTTP status for rate limiting
  4. Consider burst limits: Allow short bursts above sustained rate
  5. Key by user, not just IP: Authenticated users should have their own limits
  1. 分布式系统使用Redis:内存存储仅适用于单实例部署
  2. 设置合适的响应头:客户端需要
    X-RateLimit-*
    Retry-After
    头信息
  3. 返回429状态码:速率限制的标准HTTP状态码
  4. 考虑突发流量限制:允许在持续速率之上的短期突发请求
  5. 基于用户而非仅IP设置标识:已认证用户应拥有独立的速率限制

Common Mistakes

常见错误

  • Using fixed windows (causes burst at boundaries)
  • Not handling Redis failures gracefully
  • Forgetting to set response headers
  • Rate limiting health check endpoints
  • Not differentiating authenticated vs anonymous users
  • 使用固定窗口(会导致窗口边界处的突发流量问题)
  • 未优雅处理Redis故障
  • 忘记设置响应头
  • 对健康检查接口进行速率限制
  • 未区分已认证用户与匿名用户

Integration with Stripe Tiers

与Stripe订阅层级集成

typescript
// Resolve tier from Stripe subscription
const tierResolver = async (req: Request): Promise<string> => {
  const user = req.user;
  if (!user?.stripeSubscriptionId) return 'free';
  
  const subscription = await stripe.subscriptions.retrieve(
    user.stripeSubscriptionId
  );
  
  const priceId = subscription.items.data[0]?.price.id;
  return PRICE_TO_TIER[priceId] || 'free';
};
typescript
// 从Stripe订阅信息中解析用户层级
const tierResolver = async (req: Request): Promise<string> => {
  const user = req.user;
  if (!user?.stripeSubscriptionId) return 'free';
  
  const subscription = await stripe.subscriptions.retrieve(
    user.stripeSubscriptionId
  );
  
  const priceId = subscription.items.data[0]?.price.id;
  return PRICE_TO_TIER[priceId] || 'free';
};