rate-limiting
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRate 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
基于层级的限制
| Tier | Requests/min | Burst |
|---|---|---|
| Free | 60 | 10 |
| Pro | 600 | 100 |
| Enterprise | 6000 | 1000 |
| 层级 | 请求数/分钟 | 突发流量 |
|---|---|---|
| 免费版 | 60 | 10 |
| 专业版 | 600 | 100 |
| 企业版 | 6000 | 1000 |
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
undefinedpython
undefinedrate_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),
)undefinedimport 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),
)undefinedFastAPI Middleware
FastAPI 中间件
python
undefinedpython
undefinedfastapi_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 responseundefinedfrom 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 responseundefinedIn-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
最佳实践
- Use Redis for distributed systems: In-memory only works for single instances
- Set appropriate headers: Clients need and
X-RateLimit-*Retry-After - Return 429 status: Standard HTTP status for rate limiting
- Consider burst limits: Allow short bursts above sustained rate
- Key by user, not just IP: Authenticated users should have their own limits
- 分布式系统使用Redis:内存存储仅适用于单实例部署
- 设置合适的响应头:客户端需要和
X-RateLimit-*头信息Retry-After - 返回429状态码:速率限制的标准HTTP状态码
- 考虑突发流量限制:允许在持续速率之上的短期突发请求
- 基于用户而非仅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';
};