Trung bình Bảo Mật

Rate Limiting API - Token Bucket Hay Sliding Window

Fixed window để client burst gấp đôi ngay ranh giới. Xem token bucket với sliding window hoạt động thiệt sự ra sao, kèm cách làm với Redis và mấy cái bẫy hay dính khi lên production.

21 Th07, 2026 3 phút 353 Lượt xem 3 Khối code
Sơ đồ
graph TD A[Incoming Request] --> B{Rate Limiter Check} B --> C[Load Bucket State from Redis] C --> D{Tokens Available?} D -->|Yes| E[Consume Token] E --> F[Forward Request to API] F --> G[Return 200 + Rate Limit Headers] D -->|No| H[Reject Request] H --> I[Return 429 Too Many Requests] I --> J[Set Retry-After Header] K[Background Refill Timer] --> C

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

graph TD A[Request Tới] --> B{Kiểm Tra Rate Limiter} B --> C[Load Trạng Thái Bucket Từ Redis] C --> D{Còn Token Không?} D -->|Có| E[Tiêu Token] E --> F[Chuyển Request Tới API] F --> G[Trả 200 + Header Rate Limit] D -->|Không| H[Từ Chối Request] H --> I[Trả 429 Too Many Requests] I --> J[Set Header Retry-After] K[Timer Nạp Token Nền] --> C

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.

Cùng chủ đề Bảo Mật