Bốn thuật toán thường gặp: fixed window (đếm theo mốc thời gian cố định — rẻ nhất nhưng cho lọt gấp đôi trần ở ranh giới hai cửa sổ), sliding window (mượt hơn, tốn bộ nhớ hơn), token bucket (nạp token đều, cho tiêu dồn tới trần), leaky bucket (san phẳng tốc độ đầu ra). Các quyết định khi triển khai cho một API công khai đang bị gọi quá nhiều:
- Chọn token bucket cho traffic chung: client hợp lệ thường có burst ngắn (mở trang gọi 5–7 request) — bucket cho tiêu dồn tới trần nhưng vẫn ghìm tốc độ trung bình. Endpoint nhạy cảm (login, OTP, gửi mail) thì siết bằng cửa sổ đếm chặt, trần thấp riêng.
- Key theo gì: đã auth → per-API-key/per-user (công bằng, không phạt oan văn phòng/4G chung IP); chưa auth → per-IP nhưng nới trần vì NAT; login → key kép account + IP để chống cả đổi IP lẫn spray nhiều account.
- Đặt ở đâu: tại API gateway / reverse proxy / edge — chặn trước khi request tốn tài nguyên app. Counter bắt buộc ở store chia sẻ (Redis): đếm cục bộ từng instance thì trần thực tế = limit × số instance.
- Response khi bị chặn:
429 Too Many Requests+Retry-After(số giây chờ) +RateLimit-*báo quota còn lại — thiếuRetry-Afterthì client retry dồn dập đúng lúc hệ đang quá tải.
js
// fixed-window counter in Redis: INCR + EXPIRE per key + window
const k = "rl:" + apiKey + ":" + Math.floor(Date.now() / 60000) // 1-min bucket
const n = await redis.incr(k)
if (n === 1) await redis.expire(k, 60)
if (n > 100) {
res.setHeader("Retry-After", "60")
return res.sendStatus(429)
}Edge case: INCR + EXPIRE là hai lệnh — chết giữa chừng để lại key không TTL → gói vào Lua script cho atomic. Đừng cho health check và webhook của đối tác chung bucket với user thường — kẻo tự chặn chính mình.
Lỗi hay gặp: giới hạn theo IP khi user sau NAT; đếm cục bộ từng instance; quên Retry-After.