Rate Limiting API: Token Bucket Hay Sliding Window
Không có rate limiting, một client "quậy" thôi cũng đủ làm API tậm tịt cho tất cả người khác. Nhưng cách làm rate limiting kiểu ngây thơ lại dính một lỗi biên khá nguy hiểm mà đa số team chỉ phát hiện ra khi đã lên production.
Vấn đề: Fixed Window Counter
Cách đơn giản nhất là đếm request trong một khung thời gian cố định, rồi reset counter mỗi kỳ:
// Fixed window ngây thơ với Redis
async function isAllowed(userId) {
const key = `rl:${userId}:${Math.floor(Date.now() / 60000)}` // bucket theo phút
const count = await redis.incr(key)
await redis.expire(key, 60)
return count <= 100 // 100 request/phút
}
Nhìn thì ổn, cho tới khi client gửi 100 request lúc 0:59 rồi thêm 100 request lúc 1:00 - vậy là 200 request trong 2 giây, gấp đôi giới hạn dự tính, chỉ vì nó nằm vắt qua ranh giới của window.
Token Bucket
Một cái bucket chứa tối đa N token. Token được nạp lại theo tốc độ cố định. Mỗi request tiêu tốn một token; hết token là request bị từ chối. Cách này cho phép burst ngắn (tới giới hạn bucket) mà vẫn giữ được tốc độ trung bình nghiêm ngặt theo thời gian - đây chính là thứ Stripe với AWS API Gateway đang xài bên dưới.
class TokenBucket {
constructor(capacity, refillPerSecond) {
this.capacity = capacity
this.tokens = capacity
this.refillPerSecond = refillPerSecond
this.lastRefill = Date.now()
}
tryConsume() {
this.refill()
if (this.tokens < 1) return false
this.tokens -= 1
return true
}
refill() {
const now = Date.now()
const elapsedSeconds = (now - this.lastRefill) / 1000
this.tokens = Math.min(this.capacity, this.tokens + elapsedSeconds * this.refillPerSecond)
this.lastRefill = now
}
}
Sliding Window Counter
Một lựa chọn rẻ hơn so với sliding log đầy đủ: gán trọng số cho count của window trước dựa theo mức độ nó còn "chồng lấn" lên thời điểm hiện tại.
// trung bình có trọng số giữa window trước và window hiện tại
const weight = 1 - (elapsedInCurrentWindow / windowSize)
const estimatedCount = previousWindowCount * weight + currentWindowCount
Cách này làm mượt vấn đề burst ở ranh giới mà không tốn bộ nhớ để lưu timestamp cho từng request.
Luồng Của Một Request Bị Rate Limit
Khi Nào Xài Cái Nào
- Fixed window: dễ làm nhất, hợp với giới hạn thô kiểu theo ngày/theo giờ, chỗ nào burst không quan trọng lắm.
- Token bucket: lựa chọn mặc định cho API public - cho phép burst hợp lý mà vẫn giữ tốc độ trung bình nghiêm ngặt.
- Sliding window: chính xác nhất khi tải liên tục, đáng bỏ thêm công sức cho SLA khắt khe hay giới hạn liên quan billing.
Mấy Cái Bẫy Hay Gặp
- Chỉ giới hạn theo IP là dính lỗi ngay với user đứng sau NAT chung hay proxy công ty - nên key thêm theo API key hoặc user ID chứ đừng chỉ IP.
- Bỏ qua header rate-limit (
X-RateLimit-Remaining,Retry-After) bắt client phải đoán mò lúc nào được retry, gây ra thundering-herd retry. - Counter in-memory theo từng instance trong hệ thống nhiều server nghĩa là mỗi instance tự giới hạn riêng - client có thể đạt gấp N lần giới hạn dự tính bằng cách đánh vào N instance khác nhau. Lúc nào cũng nên backing counter bằng shared store như Redis.
- Không rate-limit riêng endpoint đăng nhập - route login/signup cần giới hạn chặt hơn traffic API thường để làm chậm mấy vụ credential stuffing.
Bắt đầu với token bucket backing bằng Redis. Dễ suy luận, cho phép burst hợp lý, và cùng một primitive đó scale được từ một endpoint đơn lẻ tới cả một API gateway hoàn chỉnh.

