Bài trước phân biệt semaphore (giới hạn số đồng thời) với rate limiter (giới hạn tốc độ). Bài này đi sâu vào rate limiter chuẩn của Go: golang.org/x/time/rate, dựa trên thuật toán token bucket. Nó trả lời câu hỏi khác semaphore: không phải "bao nhiêu chạy cùng lúc" mà "bao nhiêu sự kiện mỗi giây" — và cho phép burst (đợt dồn ngắn). Bài này đo thật hành vi của nó: burst, tốc độ hội tụ, và ba cách lấy token.
Thuật toán token bucket
Hình dung một cái xô: chứa tối đa burst token, tự đổ đầy với tốc độ rate token mỗi giây. Mỗi sự kiện tiêu một token. Hết token thì chờ (hoặc từ chối):
lim := rate.NewLimiter(rate.Limit(5), 3)
// 5 token/giây burst=3
// → trung bình 5/giây, nhưng cho phép DỒN tối đa 3 lần liền
burst là chìa khóa: nó cho phép các đợt ngắn vượt tốc độ trung bình (miễn xô còn token), rồi kéo về đúng tốc độ khi xô cạn. Đây là cân bằng giữa "mượt hoàn toàn" (burst=1) và "cho phép bùng ngắn" (burst lớn) — quan trọng cho traffic thực tế vốn không đều.

Hình 1: Token bucket. Xô chứa burst token, nạp rate token/giây. Ba cách lấy (Allow/Wait/Reserve), ví dụ burst, và khác biệt với semaphore.
Ba cách lấy token
x/time/rate cho ba cách, mỗi cách hợp một tình huống:
lim.Allow() // KHÔNG chờ: true nếu có token, false thì bỏ (drop)
lim.Wait(ctx) // CHỜ tới khi có token (tôn trọng ctx hủy/timeout)
r := lim.Reserve() // đặt chỗ: r.Delay() cho biết PHẢI CHỜ bao lâu; r.Cancel() trả token
Allowcho API "quá tải thì trả 429 ngay" — không muốn giữ request chờ.Waitcho worker "cứ chờ rồi làm" — throttle công việc nền theo tốc độ.Reservecho logic tùy biến — biết trước delay để quyết định (chờ, bỏ, hay báo client).
Đo thật: burst và tốc độ
Burst: NewLimiter(5/giây, burst=3), gọi Allow() 5 lần liền:
→ true true true false false // 3 token đầu có sẵn, hết → bị bỏ
Xô bắt đầu đầy 3 token nên 3 sự kiện đầu qua ngay; token cạn nên các sự kiện tiếp bị từ chối cho tới khi xô nạp lại.
Tốc độ: đo bằng cách gọi Wait() nhiều lần và tính thời gian:

Hình 2: Burst (true×3 rồi false). Tốc độ: Wait 20 lần ở 5/giây burst 3 mất 3,40s (kỳ vọng (20-3)/5); Wait 200 lần ở 50/giây → 50,2/giây (hội tụ); Reserve báo chờ 0,50s. So rate limiter vs semaphore.
- Wait 20 lần ở 5/giây, burst 3: mất 3,40s ≈ (20-3)/5 (3 token burst dùng ngay, 17 còn lại ở 5/giây).
- Wait 200 lần ở 50/giây, burst 1: 3,98s → 50,2/giây — hội tụ chính xác về tốc độ cấu hình.
- Reserve khi hết token ở 2/giây: báo chờ 0,50s (= thời gian nạp 1 token) mà không block.
Token bucket giữ tốc độ chính xác như cấu hình (50,2 so với mục tiêu 50), và Reserve cho biết trước delay — công cụ chính xác để kiểm soát tốc độ.
Ứng dụng thực tế
Bảo vệ backend/API khỏi quá tải. Đặt rate limiter trước một endpoint hoặc một dependency (DB, API bên ngoài) để không bao giờ vượt tốc độ an toàn. Dùng Allow + trả 429 cho traffic vào, hoặc Wait để throttle worker gọi dependency.
Tôn trọng rate limit của API bên thứ ba. Nhiều API giới hạn "X request/giây". Đặt một rate.Limiter khớp giới hạn đó và Wait() trước mỗi lời gọi — bạn không bao giờ bị 429 từ họ, và burst cho phép tận dụng hạn ngạch khi cần.
Rate limit theo client (per-key). Giữ một map[clientID]*rate.Limiter để giới hạn từng client riêng — chống một client lạm dụng làm ảnh hưởng client khác. Nhớ dọn limiter cũ (hoặc dùng thư viện có TTL) để không rò bộ nhớ.
Đánh đổi cần cân nhắc
Rate limiter và semaphore giải hai bài toán khác nhau — thường cần cả hai. Rate limiter kiểm soát tốc độ (sự kiện/giây), semaphore kiểm soát đồng thời (số chạy cùng lúc). Một request chạy 10 phút vẫn là "1 đồng thời" nhưng 1000 request/giây tức thời là vấn đề tốc độ. Backend thực thường đặt cả hai: rate limit chặn đợt dồn, semaphore chặn quá tải đồng thời.
Chọn burst cẩn thận. Burst quá nhỏ (=1) làm traffic bị "nghẹt" cứng nhắc, không chịu được đợt tự nhiên. Burst quá lớn cho phép đợt dồn lớn có thể vẫn làm ngợp backend trong khoảnh khắc. Cân theo khả năng chịu đợt của hệ thống phía sau — thường burst = vài giây tốc độ trung bình là hợp lý.
Rate limiter này là in-process, không phân tán. x/time/rate giới hạn trong một tiến trình. Nếu bạn chạy nhiều instance sau load balancer, mỗi instance có limiter riêng — tổng tốc độ = N × giới hạn. Rate limit phân tán (chia sẻ qua Redis) là bài toán khác, phức tạp hơn nhiều; đừng nhầm limiter in-process với giới hạn toàn cục.
Ba ý mang về
- Token bucket giới hạn tốc độ với burst: một xô chứa tối đa
bursttoken, nạpratetoken/giây, mỗi sự kiện tiêu 1 — đo thật burst 3 cho 3 lầnAllow()đầu qua ngay rồi bị bỏ, và tốc độ hội tụ chính xác về cấu hình (200 sự kiện ở 50/giây → 50,2/giây). - Ba cách lấy token cho ba tình huống:
Allow(không chờ, bỏ — cho API trả 429 ngay),Wait(ctx)(chờ tới khi có token — cho worker),Reserve(báo trước delay — đo thật 0,50s khi hết token ở 2/giây, cho logic tùy biến). - Rate limiter khác semaphore — thường cần cả hai: rate limiter giới hạn tốc độ (mỗi giây, cho burst), semaphore giới hạn đồng thời (cùng lúc) — backend thực đặt cả hai; và nhớ
x/time/ratelà in-process, nhiều instance thì tổng tốc độ nhân lên.
Phần sau ta xem một kỹ thuật đồng thời giải quyết vấn đề "nhiều request cùng hỏi một thứ": Phần sau mổ xẻ singleflight — cách gộp nhiều lời gọi trùng thành một để chống thundering herd (đàn sấm) khi cache miss, và đo thật hiệu quả.