正在加载,请稍候…

API Rate Limiting in Node.js: Token Bucket, Sliding Window, and Redis Strategies

Implement robust API rate limiting — fixed window, sliding window, token bucket algorithms, Redis-backed distributed limiting, and IP vs user-based strategies.

Rate Limiting Algorithms

Fixed Window

Simple but allows burst at window boundaries.

Sliding Window

Smoother distribution, counts requests in a rolling window.

Token Bucket

Allows controlled bursts while enforcing average rate.

express-rate-limit (Basic)

npm install express-rate-limit
import rateLimit from 'express-rate-limit'

// Global rate limit
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
}))

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true, // Only count failed attempts
})
app.use('/auth/login', authLimiter)

Redis-Backed Sliding Window

import Redis from 'ioredis'

const redis = new Redis()

async function slidingWindowLimit(
  key: string,
  limit: number,
  windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
  const now = Date.now()
  const windowStart = now - windowMs
  
  const pipeline = redis.pipeline()
  pipeline.zremrangebyscore(key, '-inf', windowStart)   // Remove old entries
  pipeline.zadd(key, now, `${now}-${Math.random()}`)    // Add current request
  pipeline.zcard(key)                                    // Count in window
  pipeline.pexpire(key, windowMs)                        // Set expiry
  
  const results = await pipeline.exec()
  const count = results?.[2]?.[1] as number ?? 0
  
  return {
    allowed: count <= limit,
    remaining: Math.max(0, limit - count),
    resetAt: new Date(now + windowMs),
  }
}

// Middleware
app.use(async (req, res, next) => {
  const identifier = req.user?.id ?? req.ip
  const { allowed, remaining, resetAt } = await slidingWindowLimit(
    `rate:${identifier}`,
    100,
    60 * 1000
  )
  
  res.set({
    'X-RateLimit-Limit': '100',
    'X-RateLimit-Remaining': remaining.toString(),
    'X-RateLimit-Reset': resetAt.toISOString(),
  })
  
  if (!allowed) {
    return res.status(429).json({ error: 'Rate limit exceeded' })
  }
  next()
})

Token Bucket Algorithm

class TokenBucket {
  private tokens: number
  private lastRefill: number
  
  constructor(
    private capacity: number,
    private refillRate: number,  // tokens per second
  ) {
    this.tokens = capacity
    this.lastRefill = Date.now()
  }
  
  async consume(tokens = 1): Promise<boolean> {
    this.refill()
    if (this.tokens < tokens) return false
    this.tokens -= tokens
    return true
  }
  
  private refill() {
    const now = Date.now()
    const elapsed = (now - this.lastRefill) / 1000
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate)
    this.lastRefill = now
  }
}

// Distributed token bucket with Redis Lua script
const consumeToken = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now

local elapsed = (now - last_refill) / 1000
tokens = math.min(capacity, tokens + elapsed * refill_rate)

if tokens >= requested then
  tokens = tokens - requested
  redis.call('hmset', key, 'tokens', tokens, 'last_refill', now)
  redis.call('pexpire', key, 60000)
  return 1
end
return 0
`

async function consumeTokenRedis(userId: string): Promise<boolean> {
  const result = await redis.eval(
    consumeToken, 1,
    `bucket:${userId}`,
    100,    // capacity
    10,     // refill rate (tokens/sec)
    Date.now(),
    1       // consume 1 token
  )
  return result === 1
}

Tiered Rate Limits

const tiers = {
  free:    { requests: 100,   windowMs: 60 * 60 * 1000 },
  pro:     { requests: 1000,  windowMs: 60 * 60 * 1000 },
  enterprise: { requests: 10000, windowMs: 60 * 60 * 1000 },
}

app.use(async (req, res, next) => {
  const tier = req.user?.subscription ?? 'free'
  const { requests, windowMs } = tiers[tier]
  const { allowed } = await slidingWindowLimit(`rate:${req.user?.id}`, requests, windowMs)
  if (!allowed) return res.status(429).json({ error: 'Rate limit exceeded' })
  next()
})