api-rate-limiting

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Rate Limiting

API限流

Overview

概述

Protect APIs from abuse and manage traffic using various rate limiting algorithms with per-user, per-IP, and per-endpoint strategies.
使用多种限流算法(支持按用户、按IP、按端点的策略)保护API免受滥用并管理流量。

When to Use

适用场景

  • Protecting APIs from brute force attacks
  • Managing traffic spikes
  • Implementing tiered service plans
  • Preventing DoS attacks
  • Fairness in resource allocation
  • Enforcing quotas and usage limits
  • 保护API免受暴力攻击
  • 应对流量峰值
  • 实现分层服务方案
  • 防止DoS攻击
  • 资源分配公平性保障
  • 执行配额与使用限制

Instructions

实现说明

1. Token Bucket Algorithm

1. 令牌桶算法

javascript
// Token Bucket Rate Limiter
class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillRate; // tokens per second
    this.lastRefillTime = Date.now();
  }

  refill() {
    const now = Date.now();
    const timePassed = (now - this.lastRefillTime) / 1000;
    const tokensToAdd = timePassed * this.refillRate;

    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefillTime = now;
  }

  consume(tokens = 1) {
    this.refill();

    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  available() {
    this.refill();
    return Math.floor(this.tokens);
  }
}

// Express middleware
const express = require('express');
const app = express();

const rateLimiters = new Map();

const tokenBucketRateLimit = (capacity, refillRate) => {
  return (req, res, next) => {
    const key = req.user?.id || req.ip;

    if (!rateLimiters.has(key)) {
      rateLimiters.set(key, new TokenBucket(capacity, refillRate));
    }

    const limiter = rateLimiters.get(key);

    if (limiter.consume(1)) {
      res.setHeader('X-RateLimit-Limit', capacity);
      res.setHeader('X-RateLimit-Remaining', limiter.available());
      next();
    } else {
      res.status(429).json({
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil(1 / limiter.refillRate)
      });
    }
  };
};

app.get('/api/data', tokenBucketRateLimit(100, 10), (req, res) => {
  res.json({ data: 'api response' });
});
javascript
// 令牌桶限流器
class TokenBucket {
  constructor(capacity, refillRate) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillRate; // tokens per second
    this.lastRefillTime = Date.now();
  }

  refill() {
    const now = Date.now();
    const timePassed = (now - this.lastRefillTime) / 1000;
    const tokensToAdd = timePassed * this.refillRate;

    this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
    this.lastRefillTime = now;
  }

  consume(tokens = 1) {
    this.refill();

    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }

  available() {
    this.refill();
    return Math.floor(this.tokens);
  }
}

// Express 中间件
const express = require('express');
const app = express();

const rateLimiters = new Map();

const tokenBucketRateLimit = (capacity, refillRate) => {
  return (req, res, next) => {
    const key = req.user?.id || req.ip;

    if (!rateLimiters.has(key)) {
      rateLimiters.set(key, new TokenBucket(capacity, refillRate));
    }

    const limiter = rateLimiters.get(key);

    if (limiter.consume(1)) {
      res.setHeader('X-RateLimit-Limit', capacity);
      res.setHeader('X-RateLimit-Remaining', limiter.available());
      next();
    } else {
      res.status(429).json({
        error: 'Rate limit exceeded',
        retryAfter: Math.ceil(1 / limiter.refillRate)
      });
    }
  };
};

app.get('/api/data', tokenBucketRateLimit(100, 10), (req, res) => {
  res.json({ data: 'api response' });
});

2. Sliding Window Algorithm

2. 滑动窗口算法

javascript
class SlidingWindowLimiter {
  constructor(maxRequests, windowSizeSeconds) {
    this.maxRequests = maxRequests;
    this.windowSize = windowSizeSeconds * 1000; // Convert to ms
    this.requests = [];
  }

  isAllowed() {
    const now = Date.now();
    const windowStart = now - this.windowSize;

    // Remove old requests outside window
    this.requests = this.requests.filter(time => time > windowStart);

    if (this.requests.length < this.maxRequests) {
      this.requests.push(now);
      return true;
    }
    return false;
  }

  remaining() {
    const now = Date.now();
    const windowStart = now - this.windowSize;
    this.requests = this.requests.filter(time => time > windowStart);
    return Math.max(0, this.maxRequests - this.requests.length);
  }
}

const slidingWindowRateLimit = (maxRequests, windowSeconds) => {
  const limiters = new Map();

  return (req, res, next) => {
    const key = req.user?.id || req.ip;

    if (!limiters.has(key)) {
      limiters.set(key, new SlidingWindowLimiter(maxRequests, windowSeconds));
    }

    const limiter = limiters.get(key);

    if (limiter.isAllowed()) {
      res.setHeader('X-RateLimit-Limit', maxRequests);
      res.setHeader('X-RateLimit-Remaining', limiter.remaining());
      next();
    } else {
      res.status(429).json({ error: 'Rate limit exceeded' });
    }
  };
};

app.get('/api/search', slidingWindowRateLimit(30, 60), (req, res) => {
  res.json({ results: [] });
});
javascript
class SlidingWindowLimiter {
  constructor(maxRequests, windowSizeSeconds) {
    this.maxRequests = maxRequests;
    this.windowSize = windowSizeSeconds * 1000; // Convert to ms
    this.requests = [];
  }

  isAllowed() {
    const now = Date.now();
    const windowStart = now - this.windowSize;

    // Remove old requests outside window
    this.requests = this.requests.filter(time => time > windowStart);

    if (this.requests.length < this.maxRequests) {
      this.requests.push(now);
      return true;
    }
    return false;
  }

  remaining() {
    const now = Date.now();
    const windowStart = now - this.windowSize;
    this.requests = this.requests.filter(time => time > windowStart);
    return Math.max(0, this.maxRequests - this.requests.length);
  }
}

const slidingWindowRateLimit = (maxRequests, windowSeconds) => {
  const limiters = new Map();

  return (req, res, next) => {
    const key = req.user?.id || req.ip;

    if (!limiters.has(key)) {
      limiters.set(key, new SlidingWindowLimiter(maxRequests, windowSeconds));
    }

    const limiter = limiters.get(key);

    if (limiter.isAllowed()) {
      res.setHeader('X-RateLimit-Limit', maxRequests);
      res.setHeader('X-RateLimit-Remaining', limiter.remaining());
      next();
    } else {
      res.status(429).json({ error: 'Rate limit exceeded' });
    }
  };
};

app.get('/api/search', slidingWindowRateLimit(30, 60), (req, res) => {
  res.json({ results: [] });
});

3. Redis-Based Rate Limiting

3. 基于Redis的限流

javascript
const redis = require('redis');
const client = redis.createClient();

// Sliding window with Redis
const redisRateLimit = (maxRequests, windowSeconds) => {
  return async (req, res, next) => {
    const key = `ratelimit:${req.user?.id || req.ip}`;
    const now = Date.now();
    const windowStart = now - (windowSeconds * 1000);

    try {
      // Remove old requests
      await client.zremrangebyscore(key, 0, windowStart);

      // Count requests in window
      const count = await client.zcard(key);

      if (count < maxRequests) {
        // Add current request
        await client.zadd(key, now, `${now}-${Math.random()}`);
        // Set expiration
        await client.expire(key, windowSeconds);

        res.setHeader('X-RateLimit-Limit', maxRequests);
        res.setHeader('X-RateLimit-Remaining', maxRequests - count - 1);
        next();
      } else {
        const oldestRequest = await client.zrange(key, 0, 0);
        const resetTime = parseInt(oldestRequest[0]) + (windowSeconds * 1000);
        const retryAfter = Math.ceil((resetTime - now) / 1000);

        res.set('Retry-After', retryAfter);
        res.status(429).json({
          error: 'Rate limit exceeded',
          retryAfter
        });
      }
    } catch (error) {
      console.error('Rate limit error:', error);
      next(); // Allow request if Redis fails
    }
  };
};

app.get('/api/expensive', redisRateLimit(10, 60), (req, res) => {
  res.json({ result: 'expensive operation' });
});
javascript
const redis = require('redis');
const client = redis.createClient();

// Sliding window with Redis
const redisRateLimit = (maxRequests, windowSeconds) => {
  return async (req, res, next) => {
    const key = `ratelimit:${req.user?.id || req.ip}`;
    const now = Date.now();
    const windowStart = now - (windowSeconds * 1000);

    try {
      // Remove old requests
      await client.zremrangebyscore(key, 0, windowStart);

      // Count requests in window
      const count = await client.zcard(key);

      if (count < maxRequests) {
        // Add current request
        await client.zadd(key, now, `${now}-${Math.random()}`);
        // Set expiration
        await client.expire(key, windowSeconds);

        res.setHeader('X-RateLimit-Limit', maxRequests);
        res.setHeader('X-RateLimit-Remaining', maxRequests - count - 1);
        next();
      } else {
        const oldestRequest = await client.zrange(key, 0, 0);
        const resetTime = parseInt(oldestRequest[0]) + (windowSeconds * 1000);
        const retryAfter = Math.ceil((resetTime - now) / 1000);

        res.set('Retry-After', retryAfter);
        res.status(429).json({
          error: 'Rate limit exceeded',
          retryAfter
        });
      }
    } catch (error) {
      console.error('Rate limit error:', error);
      next(); // Allow request if Redis fails
    }
  };
};

app.get('/api/expensive', redisRateLimit(10, 60), (req, res) => {
  res.json({ result: 'expensive operation' });
});

4. Tiered Rate Limiting

4. 分层限流

javascript
const RATE_LIMITS = {
  free: { requests: 100, window: 3600 },      // 100 per hour
  pro: { requests: 10000, window: 3600 },     // 10,000 per hour
  enterprise: { requests: null, window: null } // Unlimited
};

const tieredRateLimit = async (req, res, next) => {
  const user = req.user;
  const plan = user?.plan || 'free';
  const limits = RATE_LIMITS[plan];

  if (!limits.requests) {
    return next(); // Unlimited plan
  }

  const key = `ratelimit:${user.id}`;
  const now = Date.now();
  const windowStart = now - (limits.window * 1000);

  try {
    await client.zremrangebyscore(key, 0, windowStart);
    const count = await client.zcard(key);

    if (count < limits.requests) {
      await client.zadd(key, now, `${now}-${Math.random()}`);
      await client.expire(key, limits.window);

      res.setHeader('X-RateLimit-Limit', limits.requests);
      res.setHeader('X-RateLimit-Remaining', limits.requests - count - 1);
      res.setHeader('X-Plan', plan);
      next();
    } else {
      res.status(429).json({
        error: 'Rate limit exceeded',
        plan,
        upgradeUrl: '/plans'
      });
    }
  } catch (error) {
    next();
  }
};

app.use(tieredRateLimit);
javascript
const RATE_LIMITS = {
  free: { requests: 100, window: 3600 },      // 100 per hour
  pro: { requests: 10000, window: 3600 },     // 10,000 per hour
  enterprise: { requests: null, window: null } // Unlimited
};

const tieredRateLimit = async (req, res, next) => {
  const user = req.user;
  const plan = user?.plan || 'free';
  const limits = RATE_LIMITS[plan];

  if (!limits.requests) {
    return next(); // Unlimited plan
  }

  const key = `ratelimit:${user.id}`;
  const now = Date.now();
  const windowStart = now - (limits.window * 1000);

  try {
    await client.zremrangebyscore(key, 0, windowStart);
    const count = await client.zcard(key);

    if (count < limits.requests) {
      await client.zadd(key, now, `${now}-${Math.random()}`);
      await client.expire(key, limits.window);

      res.setHeader('X-RateLimit-Limit', limits.requests);
      res.setHeader('X-RateLimit-Remaining', limits.requests - count - 1);
      res.setHeader('X-Plan', plan);
      next();
    } else {
      res.status(429).json({
        error: 'Rate limit exceeded',
        plan,
        upgradeUrl: '/plans'
      });
    }
  } catch (error) {
    next();
  }
};

app.use(tieredRateLimit);

5. Python Rate Limiting (Flask)

5. Python限流实现(Flask)

python
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from datetime import datetime, timedelta
import redis

app = Flask(__name__)
limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)
python
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from datetime import datetime, timedelta
import redis

app = Flask(__name__)
limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

Custom rate limit based on user plan

Custom rate limit based on user plan

redis_client = redis.Redis(host='localhost', port=6379)
def get_rate_limit(user_id): plan = redis_client.get(f'user:{user_id}:plan').decode() limits = { 'free': (100, 3600), 'pro': (10000, 3600), 'enterprise': (None, None) } return limits.get(plan, (100, 3600))
@app.route('/api/data', methods=['GET']) @limiter.limit("30 per minute") def get_data(): return jsonify({'data': 'api response'}), 200
@app.route('/api/premium', methods=['GET']) def get_premium_data(): user_id = request.user_id max_requests, window = get_rate_limit(user_id)
if max_requests is None:
    return jsonify({'data': 'unlimited data'}), 200

key = f'ratelimit:{user_id}'
current = redis_client.incr(key)
redis_client.expire(key, window)

if current <= max_requests:
    return jsonify({'data': 'premium data'}), 200
else:
    return jsonify({'error': 'Rate limit exceeded'}), 429
undefined
redis_client = redis.Redis(host='localhost', port=6379)
def get_rate_limit(user_id): plan = redis_client.get(f'user:{user_id}:plan').decode() limits = { 'free': (100, 3600), 'pro': (10000, 3600), 'enterprise': (None, None) } return limits.get(plan, (100, 3600))
@app.route('/api/data', methods=['GET']) @limiter.limit("30 per minute") def get_data(): return jsonify({'data': 'api response'}), 200
@app.route('/api/premium', methods=['GET']) def get_premium_data(): user_id = request.user_id max_requests, window = get_rate_limit(user_id)
if max_requests is None:
    return jsonify({'data': 'unlimited data'}), 200

key = f'ratelimit:{user_id}'
current = redis_client.incr(key)
redis_client.expire(key, window)

if current <= max_requests:
    return jsonify({'data': 'premium data'}), 200
else:
    return jsonify({'error': 'Rate limit exceeded'}), 429
undefined

6. Response Headers

6. 响应头

javascript
// Standard rate limit headers
res.setHeader('X-RateLimit-Limit', maxRequests);          // Total requests allowed
res.setHeader('X-RateLimit-Remaining', remaining);        // Remaining requests
res.setHeader('X-RateLimit-Reset', resetTime);            // Unix timestamp of reset
res.setHeader('Retry-After', secondsToWait);              // How long to wait

// 429 Too Many Requests response
{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "retryAfter": 60,
  "resetAt": "2025-01-15T15:00:00Z"
}
javascript
// 标准限流响应头
res.setHeader('X-RateLimit-Limit', maxRequests);          // 允许的总请求数
res.setHeader('X-RateLimit-Remaining', remaining);        // 剩余可用请求数
res.setHeader('X-RateLimit-Reset', resetTime);            // 重置时间的Unix时间戳
res.setHeader('Retry-After', secondsToWait);              // 需要等待的秒数

// 429请求过多响应
{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "retryAfter": 60,
  "resetAt": "2025-01-15T15:00:00Z"
}

Best Practices

最佳实践

✅ DO

✅ 建议

  • Include rate limit headers in responses
  • Use Redis for distributed rate limiting
  • Implement tiered limits for different user plans
  • Set appropriate window sizes and limits
  • Monitor rate limit metrics
  • Provide clear retry guidance
  • Document rate limits in API docs
  • Test under high load
  • 在响应中包含限流响应头
  • 使用Redis实现分布式限流
  • 为不同用户套餐实现分层限流
  • 设置合适的窗口大小和限流阈值
  • 监控限流指标
  • 提供清晰的重试指引
  • 在API文档中说明限流规则
  • 在高负载场景下测试

❌ DON'T

❌ 不建议

  • Use in-memory storage in production
  • Set limits too restrictively
  • Forget to include Retry-After header
  • Ignore distributed scenarios
  • Make rate limits public (security)
  • Use simple counters for distributed systems
  • Forget cleanup of old data
  • 在生产环境中使用内存存储限流数据
  • 设置过于严格的限流阈值
  • 遗漏Retry-After响应头
  • 忽略分布式场景的适配
  • 公开限流阈值(安全风险)
  • 在分布式系统中使用简单计数器
  • 忘记清理过期数据

Monitoring

监控实现

javascript
// Track rate limit metrics
const metrics = {
  totalRequests: 0,
  limitedRequests: 0,
  byUser: new Map()
};

app.use((req, res, next) => {
  metrics.totalRequests++;
  res.on('finish', () => {
    if (res.statusCode === 429) {
      metrics.limitedRequests++;
    }
  });
  next();
});

app.get('/metrics/rate-limit', (req, res) => {
  res.json({
    totalRequests: metrics.totalRequests,
    limitedRequests: metrics.limitedRequests,
    percentage: (metrics.limitedRequests / metrics.totalRequests * 100).toFixed(2)
  });
});
javascript
// 跟踪限流指标
const metrics = {
  totalRequests: 0,
  limitedRequests: 0,
  byUser: new Map()
};

app.use((req, res, next) => {
  metrics.totalRequests++;
  res.on('finish', () => {
    if (res.statusCode === 429) {
      metrics.limitedRequests++;
    }
  });
  next();
});

app.get('/metrics/rate-limit', (req, res) => {
  res.json({
    totalRequests: metrics.totalRequests,
    limitedRequests: metrics.limitedRequests,
    percentage: (metrics.limitedRequests / metrics.totalRequests * 100).toFixed(2)
  });
});