Thuật toán, từ đơn giản đến chính xác:
- Fixed window — đếm request theo từng phút. Đơn giản nhất nhưng có lỗi biên: 100 request cuối phút 1 và 100 request đầu phút 2 → 200 request trong 2 giây vẫn hợp lệ.
- Sliding window log/counter — chính xác hơn, sliding log lưu timestamp từng request (tốn bộ nhớ), sliding counter nội suy từ window trước (đủ tốt, rẻ).
- Token bucket — bucket chứa N token, nạp lại đều đặn r token/giây; mỗi request tiêu 1 token. Cho phép burst tới sức chứa bucket rồi ép về tốc độ trung bình. Đây là lựa chọn mặc định cho API vì hợp với hành vi client thật.
- Leaky bucket — làm phẳng hoàn toàn, output đều tăm tắp, không cho burst; hợp với việc bảo vệ downstream có công suất cố định.
Response khi vượt hạn: trả 429 Too Many Requests kèm Retry-After (số giây hoặc mốc thời gian). Kèm theo bộ header quota để client tự điều tiết mà không cần bị chặn:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
RateLimit-Limit: 5000
RateLimit-Remaining: 0
RateLimit-Reset: 42GitHub trả bộ tương đương (x-ratelimit-limit/remaining/reset) và khuyến nghị client tôn trọng retry-after, không retry trước thời điểm đó.
Còn lại là chuyện thiết kế: đếm theo cái gì (API key > user id > IP, vì NAT làm nhiều user chung IP), lưu counter ở Redis để nhiều instance dùng chung, và tách hạn mức riêng cho endpoint đắt (search, export) thay vì một hạn mức chung cho mọi thứ.