Rate limiting: chặn lạm dụng API bằng token bucket và sliding window

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Một API công khai mà không giới hạn tần suất giống cửa hàng mở toang không bảo vệ: một script lỗi hoặc một kẻ xấu có thể làm sập dịch vụ của bạn chỉ trong vài phút. Rate limiting đặt trần số lần gọi để bảo vệ hệ thống và đảm bảo công bằng giữa người dùng.

Vì sao cần rate limiting?

  • Chống lạm dụng và tấn công: dò mật khẩu, cào dữ liệu, làm quá tải.
  • Bảo vệ tài nguyên: database và dịch vụ phía sau không bị bão request làm nghẽn.
  • Công bằng: một người dùng không chiếm hết năng lực của mọi người khác.
  • Kiểm soát chi phí: đặc biệt khi mỗi lần gọi tốn tiền (gửi SMS, gọi dịch vụ AI).

Giới hạn theo ai?

Bạn chọn "khóa" để đếm: theo địa chỉ IP (cho người chưa đăng nhập), theo id người dùng hoặc API key (cho người đã xác thực), hoặc theo endpoint (endpoint đăng nhập nên chặt hơn endpoint đọc). Lưu ý nhiều người có thể dùng chung một IP (mạng công ty, NAT của nhà mạng), nên giới hạn theo IP cần thoáng hơn.

Fixed window: đơn giản nhất

Chia thời gian thành các khung cố định (ví dụ mỗi phút) và đếm số request trong khung hiện tại. Với Redis:

import time, redisr = redis.Redis()def allow(key, limit=60, window=60):    bucket = f"rl:{key}:{int(time.time() // window)}"    count = r.incr(bucket)    if count == 1:        r.expire(bucket, window)    return count <= limit

Nhược điểm: hiện tượng "bùng nổ ở biên khung". Người dùng có thể gửi 60 request ở giây cuối của phút này và 60 request ở giây đầu của phút sau, tức 120 request trong vài giây.

Sliding window

Đếm số request trong khoảng thời gian trượt (ví dụ 60 giây gần nhất) thay vì khung cố định, mượt hơn và chính xác hơn. Cách cài đặt phổ biến là dùng sorted set của Redis, lưu mỗi request với điểm là mốc thời gian:

def allow_sliding(key, limit=60, window=60):    now = time.time()    k = f"rl:{key}"    pipe = r.pipeline()    pipe.zremrangebyscore(k, 0, now - window)   # bỏ request quá cũ    pipe.zadd(k, {str(now): now})    pipe.zcard(k)    pipe.expire(k, window)    _, _, count, _ = pipe.execute()    return count <= limit

Chính xác hơn nhưng tốn bộ nhớ hơn với giới hạn lớn, vì lưu từng request.

Token bucket

Hình dung một cái xô chứa tối đa N token. Mỗi request lấy một token; token được bổ sung đều đặn theo tốc độ cố định. Khi xô rỗng, request bị từ chối. Mẫu này cho phép đột biến ngắn (dùng hết token tích lũy) nhưng vẫn giữ tốc độ trung bình dài hạn, nên rất tự nhiên với lưu lượng thực tế.

class TokenBucket:    def __init__(self, capacity, refill_per_sec):        self.capacity = capacity        self.tokens = capacity        self.rate = refill_per_sec        self.last = time.time()    def allow(self):        now = time.time()        self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)        self.last = now        if self.tokens >= 1:            self.tokens -= 1            return True        return False

Ví dụ trên chạy trong bộ nhớ một tiến trình. Với nhiều server, trạng thái phải nằm ở nơi dùng chung như Redis và thao tác kiểm tra cần nguyên tử (thường dùng script Lua) để tránh đua dữ liệu.

Phản hồi khi vượt giới hạn

Trả về mã 429 Too Many Requests và cho client biết tình hình qua header:

HTTP/1.1 429 Too Many RequestsRetry-After: 30X-RateLimit-Limit: 60X-RateLimit-Remaining: 0

Một số API dùng header RateLimit-* theo chuẩn đề xuất mới. Điều quan trọng là client biết khi nào được thử lại. Phía client cũng nên tôn trọng và dùng backoff khi nhận 429.

Dùng sẵn trong framework

  • Laravel: middleware throttle:60,1 hoặc định nghĩa RateLimiter::for().
  • Express: thư viện express-rate-limit.
  • Nginx: limit_req_zone giới hạn ngay ở lớp proxy, trước khi tới ứng dụng.
  • API gateway / CDN: nhiều dịch vụ có tính năng sẵn.
// Laravel: giới hạn đăng nhập chặt hơnRateLimiter::for('login', function (Request $request) {    return Limit::perMinute(5)->by($request->ip() . '|' . $request->input('email'));});

Những điều dễ bị bỏ sót

Khi ứng dụng đứng sau proxy hoặc CDN, địa chỉ IP bạn thấy có thể là của proxy chứ không phải của người dùng thật. Hãy cấu hình danh sách proxy tin cậy để đọc đúng IP từ header như X-Forwarded-For, nếu không toàn bộ người dùng sẽ bị tính chung một bộ đếm và bị chặn hàng loạt. Ngược lại, đừng tin mù quáng header này nếu nó có thể bị giả mạo bởi chính client. Hãy ghi log mỗi khi giới hạn bị kích hoạt để phân biệt lưu lượng bất thường với người dùng thật gặp bất tiện, và điều chỉnh ngưỡng dựa trên dữ liệu chứ không phải phỏng đoán.

Kết luận

Hãy bắt đầu với giới hạn đơn giản ở lớp proxy hoặc middleware, đặt chặt hơn cho các endpoint nhạy cảm như đăng nhập, đặt lại mật khẩu, gửi OTP. Khi nhu cầu lớn dần, chuyển sang token bucket hoặc sliding window dùng Redis. Rate limiting không phải thứ xa xỉ, mà là phần cơ bản của một API sẵn sàng cho thế giới thật.

Khi cần một lộ trình học có hệ thống, bạn có thể tham khảo khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment