Mục lục
- Mục Tiêu Bài Học
- Rate Limiting Là Gì
- 4 Lý Do Cần Rate Limiting
- Vì Sao Redis Cho Rate Limiting
- Vị Trí Áp Dụng Theo Layer
- Các Chiều Rate Limit
- HTTP 429 & Response Headers Chuẩn
- Preview 5 Thuật Toán
- Trade-off Business
- Khi Nào Chưa — Khi Nào Bắt Buộc
- Preview Incident: Race Condition Không Atomic
- Lộ Trình Module 3 & Tổng Kết
Mục Tiêu Bài Học
- Định nghĩa chính xác rate limiting và phân biệt với throttling.
- Giải thích được 4 lý do cần rate limiting: chống abuse, kiểm soát cost, fair usage, bảo vệ downstream.
- Hiểu tại sao Redis (in-memory, atomic, TTL, distributed) là lựa chọn phù hợp cho rate limiter so với biến đếm in-process.
- Biết đặt rate limit ở đúng layer (Edge, API Gateway, Application, Database) theo mục đích.
- Liệt kê được các chiều limit (per IP, per user, per API key, per endpoint, global, composite) và dùng chiều nào cho bài toán nào.
- Trả về HTTP 429 đúng cách với các header
X-RateLimit-*vàRetry-After. - Nhận biết sơ bộ 5 thuật toán rate limiting sẽ được đào sâu từ bài 33, cùng trade-off chính của từng loại.
Rate Limiting Là Gì
Rate limiting là kỹ thuật kiểm soát tần suất request: giới hạn số lần một client (IP, user, API key…) được thực hiện một thao tác trong một khoảng thời gian nhất định.
Ví dụ cụ thể:
100 req/phútper user — giới hạn chung cho API.10 lần thử đăng nhập/giờper IP — bảo vệ auth endpoint.5 lần gửi OTP/ngàyper số điện thoại — kiểm soát cost SMS.50 request/giâyper API key — tier-based quota.
Throttling thường dùng hoán đổi với rate limiting nhưng đôi khi có nghĩa hẹp hơn: throttling là làm chậm (delay) request thay vì từ chối hoàn toàn. Trong tài liệu kỹ thuật, "rate limiting" bao hàm cả hai hành vi.
Rate limiting không phải chỉ cho public API. Nó cũng áp dụng cho:
- Endpoint đắt tiền (AI inference, PDF export, image resize).
- Luồng gọi nội bộ giữa các service (internal rate limiting).
- Webhook outbound (không gửi quá N webhook/giây cho mỗi client).
4 Lý Do Cần Rate Limiting
a) Chống abuse và attack
Không có rate limit, bất kỳ endpoint nào cũng là mục tiêu tấn công:
- Brute force login: thử hàng nghìn mật khẩu/phút cho cùng một tài khoản.
- Credential stuffing: dùng danh sách username/password bị rò rỉ từ dịch vụ khác để thử hàng loạt tài khoản.
- Scraping: bot thu thập dữ liệu sản phẩm, giá cả, nội dung với tốc độ hàng nghìn req/giây.
- DDoS layer 7: flood HTTP request hợp lệ về cú pháp nhưng số lượng khổng lồ, đánh vào application thay vì network.
- API spam: gọi endpoint gửi email, SMS, push notification lặp đi lặp lại.
Rate limit không thay thế firewall hay WAF, nhưng là tầng bảo vệ cần thiết ở application layer.
b) Kiểm soát cost
Một số operation có chi phí tuyến tính theo số request:
- LLM API (OpenAI, Anthropic, Gemini…): mỗi request tiêu thụ token, mỗi token có giá. Nếu không giới hạn, một user có thể tạo ra bill hàng nghìn USD chỉ trong vài phút.
- Compute-heavy endpoint: tạo ảnh, render PDF, export báo cáo — mỗi request chiếm CPU/GPU đáng kể trong vài giây.
- Bandwidth: streaming, download file lớn, bulk export — tốn egress cost trực tiếp.
- 3rd-party API có phí: tra cứu địa chỉ (Google Maps), gửi SMS (Twilio), xác minh email…
Rate limit theo cost thường gắn với tier: user free có quota nhỏ hơn user trả phí.
c) Fair usage
Không có giới hạn, một user có thể chiếm phần lớn tài nguyên của hệ thống dùng chung, ảnh hưởng trực tiếp đến trải nghiệm của những user khác:
- Một script chạy bulk import đánh sập database cho toàn bộ user khác.
- Một tài khoản poll API mỗi 100ms liên tục làm connection pool cạn kiệt.
- Bot scrape chiếm toàn bộ băng thông CDN trong giờ cao điểm.
Rate limit buộc mỗi client giữ trong phần tài nguyên được phân bổ, đảm bảo tính công bằng (fairness) cho tất cả.
d) Bảo vệ downstream
Service của bạn thường gọi đến các hệ thống khác có giới hạn riêng:
- 3rd-party API có rate limit: nếu bạn không tự giới hạn, khi user gọi nhiều bạn sẽ bị provider throttle — toàn bộ user đều bị ảnh hưởng thay vì chỉ user gây ra vấn đề.
- Database: một endpoint gây N+1 query có thể tạo query storm, làm connection pool cạn. Rate limit endpoint đó bảo vệ DB khỏi tải đột biến.
- Internal services: service A gọi service B; nếu A không tự giới hạn, B có thể bị overload khi A bị tấn công.
Vì Sao Redis Cho Rate Limiting
Rate limiter cần đếm số request theo một window thời gian. Có vẻ đơn giản, nhưng khi triển khai thực tế xuất hiện ba yêu cầu khắt khe:
- Tốc độ: mỗi request phải qua bước kiểm tra rate limit trước khi xử lý — latency của bước này phải cực thấp (dưới 1ms), không thể gọi database quan hệ.
- Thread-safe: nhiều instance application cùng đọc-tăng-ghi counter có thể tạo race condition, kết quả là counter không chính xác và giới hạn bị vượt.
- Distributed: khi scale ngang (nhiều pod/container), counter phải được chia sẻ — mỗi instance đếm riêng thì tổng vượt hạn.
Redis đáp ứng cả ba yêu cầu:
- In-memory: đọc/ghi counter trong RAM, latency cỡ 0.1–0.5ms, không có disk I/O.
- Atomic operations:
INCRtrên Redis là atomic — một lệnh duy nhất, không có trạng thái trung gian. Lua script chạy trên Redis cũng atomic (single-threaded execution). Không cần lock ở application layer. - TTL tự động: đặt
EXPIREcho counter, Redis tự xoá sau khi window kết thúc — không cần job cleanup. - Shared state: mọi instance application đều kết nối vào cùng một Redis, counter nhất quán bất kể có bao nhiêu pod.
So sánh: in-process counter vs Redis counter
# In-process counter (biến trong memory của mỗi process)
#
# Pod A: user:42 → 80 req/phút
# Pod B: user:42 → 75 req/phút (không biết về Pod A)
# Pod C: user:42 → 90 req/phút
#
# Tổng thực tế: 245 req/phút — vượt gấp 2.5x giới hạn 100
# Mỗi pod chỉ thấy phần của mình → không thể rate limit chính xác
# Redis counter (shared)
#
# Pod A: INCR rate:user:42:window → 245 → REJECT (> 100)
# Pod B: INCR rate:user:42:window → 246 → REJECT
# Pod C: INCR rate:user:42:window → 247 → REJECT
#
# Mọi pod đọc cùng counter → limit thực sự được áp dụng
In-process counter chỉ đủ khi bạn chắc chắn hệ thống sẽ mãi chạy một process duy nhất và đó là trường hợp rất hiếm trong production.
Vị Trí Áp Dụng Theo Layer
Rate limiting không phải chỉ ở một chỗ. Hệ thống production thường dùng nhiều layer (defense in depth):
| Layer | Công cụ ví dụ | Chiều limit chính | Ưu điểm | Giới hạn |
|---|---|---|---|---|
| Edge / CDN | Cloudflare, AWS CloudFront | Per IP, per ASN | Chặn rất sớm, không tốn tài nguyên origin, giảm tải CDN egress | Không biết user context, không làm business logic |
| API Gateway | Kong, AWS API Gateway, nginx | Per API key, per route | Không cần viết code, cấu hình declarative, built-in logging | Khó áp dụng business rule phức tạp (tier, quota per feature) |
| Application | Redis + code của bạn | Per user, per endpoint, composite | Biết đầy đủ context (user tier, plan, feature flag), linh hoạt nhất | Phải tự viết và maintain; Redis phải available |
| Database | Connection pool (PgBouncer, HikariCP) | Số connection đồng thời | Bảo vệ DB khỏi connection storm | Không granular theo user/endpoint |
Module 3 tập trung vào layer Application — nơi Redis thực hiện đếm và thuật toán rate limiting. Các layer trên (CDN, Gateway) bổ sung thêm nhưng không thay thế được application-level rate limiting khi cần logic theo user context.
Nguyên tắc chọn layer
- Attack volumetric (DDoS, scraping ồ ạt) → chặn ở Edge, rẻ và nhanh nhất.
- Giới hạn theo API key / plan → API Gateway.
- Giới hạn theo user, tier, feature, endpoint đặc thù → Application + Redis.
- Không nên đặt rate limit duy nhất ở Database — DB không phải nơi để làm business logic rate limit.
Các Chiều Rate Limit
"Chiều" (dimension) của rate limit xác định counter được gán cho ai hoặc cái gì. Cùng một thuật toán nhưng key Redis khác nhau tạo ra hành vi khác nhau:
| Chiều | Redis key pattern ví dụ | Dùng khi nào |
|---|---|---|
| Per IP | rl:ip:203.0.113.5:window |
Chặn anonymous abuse, bot; hiệu quả khi attacker chưa có account |
| Per user | rl:user:42:window |
Fair usage cho authenticated user; theo đúng người dùng bất kể IP |
| Per API key | rl:apikey:sk_abc123:window |
Tier-based quota; isolate client khác nhau |
| Per endpoint | rl:user:42:POST:/login:window |
Bảo vệ operation đắt tiền, nhạy cảm khác nhau mà không ảnh hưởng limit chung |
| Global | rl:global:window |
Circuit breaker toàn hệ thống khi tải vượt ngưỡng |
| Composite | Kết hợp nhiều counter song song | Áp dụng nhiều giới hạn cùng lúc: per user 1000/h AND per endpoint /export 10/h |
Trong thực tế, bạn thường kiểm tra nhiều chiều cùng lúc. Ví dụ endpoint POST /api/ai/generate có thể kiểm tra: per user (100/ngày) AND per IP (200/ngày) AND global (10.000/phút). Request bị từ chối nếu vượt bất kỳ giới hạn nào.
# Composite check — pseudo-code
async def check_rate_limit(user_id: int, ip: str, endpoint: str) -> bool:
window = current_minute()
checks = [
f"rl:user:{user_id}:{window}", # per user: 100/phút
f"rl:ip:{ip}:{window}", # per IP: 200/phút
f"rl:ep:{endpoint}:{window}", # per endpoint: 500/phút
]
limits = [100, 200, 500]
for key, limit in zip(checks, limits):
count = await redis.incr(key)
if count == 1:
await redis.expire(key, 60) # set TTL window đầu tiên
if count > limit:
return False # bị chặn
return True
Đây là pseudo-code đơn giản để minh hoạ concept. Bài 32 sẽ giải thích vì sao INCR + EXPIRE tách biệt như trên có vấn đề atomicity nghiêm trọng và cách khắc phục bằng Lua.
HTTP 429 & Response Headers Chuẩn
Khi request bị từ chối, server trả HTTP 429 Too Many Requests (RFC 6585). Đây là status code chuẩn; tránh dùng 403 Forbidden hay 503 Service Unavailable cho trường hợp này vì ý nghĩa khác nhau.
Response headers chuẩn
| Header | Ý nghĩa | Ví dụ giá trị |
|---|---|---|
X-RateLimit-Limit |
Quota tổng trong window | 100 |
X-RateLimit-Remaining |
Số request còn có thể dùng trong window hiện tại | 0 |
X-RateLimit-Reset |
Unix timestamp (giây) khi window reset | 1748434200 |
Retry-After |
Số giây client nên chờ trước khi retry (RFC 7231) | 47 |
X-RateLimit-* headers không bắt buộc trong HTTP spec nhưng đã trở thành de facto standard (GitHub, Stripe, Twilio đều dùng). Retry-After thì được định nghĩa trong RFC 7231 và nên luôn có kèm 429.
# Response ví dụ khi bị rate limit
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748434200
Retry-After: 47
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "Bạn đã vượt giới hạn 100 request/phút. Thử lại sau 47 giây.",
"retry_after": 47
}
Nên include headers ngay cả khi chưa bị limit
Trả X-RateLimit-Limit và X-RateLimit-Remaining trên mọi response (200 và 429) để client tự điều chỉnh tốc độ gọi mà không cần đợi bị từ chối. Thực hành này gọi là proactive rate limit disclosure — giúp client viết code backoff đúng thay vì retry bừa.
Preview 5 Thuật Toán
Module 3 đào sâu 5 thuật toán từ bài 33 đến 37. Phần này chỉ giới thiệu sơ bộ để đặt ngữ cảnh:
- Fixed Window (Cửa sổ cố định)
- Chia thời gian thành các window cố định (phút 0–59, phút 1–60…). Đếm request trong window; nếu vượt ngưỡng thì từ chối. Đơn giản nhất nhưng có boundary burst problem: user có thể gửi 2x quota trong vòng 2 giây ngay tại ranh giới hai window.
- Sliding Window Log (Log cửa sổ trượt)
- Lưu timestamp của từng request trong một Sorted Set. Khi check, đếm request trong khoảng
[now - window, now]. Chính xác nhất nhưng tốn bộ nhớ tuyến tính theo số request. - Sliding Window Counter (Counter cửa sổ trượt)
- Kết hợp hai window cố định liền kề với weighted average để xấp xỉ sliding window. Cân bằng giữa accuracy và memory — không cần lưu từng timestamp.
- Token Bucket (Xô token)
- Client có một "xô" chứa token. Mỗi request tiêu 1 token; token tự nạp lại theo thời gian. Xô có thể tích lũy token khi traffic thấp → cho phép burst có kiểm soát. Phổ biến nhất ở API tier-based.
- Leaky Bucket / GCRA
- Request được xử lý với tốc độ cố định (như nước nhỏ qua lỗ). Traffic đột biến được làm phẳng thay vì reject ngay. GCRA (Generic Cell Rate Algorithm) là hiện đại hóa của leaky bucket.
Bảng so sánh sơ bộ
| Thuật toán | Accuracy | Memory | Burst | Độ phức tạp |
|---|---|---|---|---|
| Fixed Window | Thấp (boundary problem) | Rất thấp (1 counter) | 2x tại ranh giới | Đơn giản |
| Sliding Window Log | Cao nhất | Cao (O(request)) | Không | Trung bình |
| Sliding Window Counter | Khá (xấp xỉ) | Thấp (2 counter) | Hạn chế | Trung bình |
| Token Bucket | Khá | Thấp | Có (controlled) | Trung bình |
| Leaky Bucket / GCRA | Smooth | Thấp | Không | Phức tạp hơn |
Không có thuật toán "tốt nhất" — lựa chọn phụ thuộc vào yêu cầu: cần phép burst → Token Bucket; cần smooth traffic → Leaky Bucket; cần đơn giản và chấp nhận trade-off → Fixed Window. Các bài 33–37 đi qua từng thuật toán với Redis implementation cụ thể.
Trade-off Business
Rate limit không chỉ là bài toán kỹ thuật. Giá trị ngưỡng, cách xử lý khi bị limit, và cách thông báo đều ảnh hưởng trực tiếp đến trải nghiệm người dùng:
Limit quá chặt
- Block user thật đang dùng hợp lý — đặc biệt nếu limit per IP mà user đứng sau NAT hoặc VPN.
- Developer tích hợp API của bạn gặp 429 liên tục trong lúc test → abandon.
- Automation hợp lệ (CI/CD, ETL, cron job) bị chặn.
Limit quá lỏng
- Không ngăn được brute force login với 10.000 lần thử/giờ.
- Cost LLM API vượt budget vì user spam endpoint AI.
- Bot scrape xong toàn bộ catalog trong vài giờ.
Tier-based quota
Cách phổ biến nhất để cân bằng: gắn limit với plan/tier của user.
# Ví dụ quota theo tier (req/giờ):
# Free tier: 100 req/giờ, 10 req/giờ cho endpoint AI
# Pro tier: 1.000 req/giờ, 100 req/giờ cho endpoint AI
# Enterprise: 10.000 req/giờ, 1.000 req/giờ cho endpoint AI
Graceful degradation
Thay vì đột ngột trả 429 khi đạt 100%, nên:
- Warn sớm: khi còn 20% quota, trả header
X-RateLimit-Warning: approaching-limit. - Retry-After rõ ràng: luôn kèm thời gian cụ thể thay vì để client tự đoán.
- Error message hữu ích: "Vượt giới hạn 100 req/phút. Reset lúc 14:32:00. Nâng lên Pro để có 1.000 req/phút." — không phải lỗi bí ẩn.
- Separate quota per feature: giới hạn endpoint AI không ảnh hưởng đến quota REST API thông thường.
Khi Nào Chưa — Khi Nào Bắt Buộc
Chưa cần rate limit ngay
- Internal tool với trusted users: admin panel chỉ có 5 người dùng nội bộ — overhead không xứng.
- Traffic thấp, không có expensive operation: prototype chưa có user thật, không có endpoint LLM hay export nặng.
- Giai đoạn MVP: ưu tiên ship nhanh; thêm rate limit khi có traffic thật và pattern abuse rõ hơn. Nhưng thiết kế hệ thống để dễ thêm vào sau — đừng hardcode logic authentication theo kiểu không thể chèn middleware.
Bắt buộc cần rate limit
- Public API: bất kỳ endpoint nào không cần xác thực đều là mục tiêu tiềm năng.
- Login / auth endpoint: brute force và credential stuffing luôn xảy ra; không có rate limit ở đây là lỗi bảo mật nghiêm trọng.
- LLM / AI endpoint: cost tuyến tính theo request, không giới hạn → bill không kiểm soát được.
- Expensive operation: export báo cáo, generate PDF, resize ảnh hàng loạt, bulk import.
- Endpoint dễ abuse: đăng ký tài khoản, gửi OTP, gửi email (invite, verification, reset password), tạo link chia sẻ.
Một quy tắc đơn giản: nếu endpoint có thể gây chi phí tuyến tính (compute, API cost, storage) hoặc có thể bị dùng để tấn công tài khoản khác — thêm rate limit.
Preview Incident: Race Condition Không Atomic
Bài 32 phân tích chi tiết vì sao rate limiter phải dùng atomic operation. Phần này preview incident cụ thể để thấy hậu quả khi không làm đúng.
Pattern sai phổ biến: GET-check-INCR tách biệt.
# WRONG: không atomic — có race condition
async def check_rate_limit_broken(key: str, limit: int) -> bool:
current = int(await redis.get(key) or 0) # GET
if current >= limit:
return False # bị chặn
await redis.incr(key) # INCR
return True # cho qua
Vấn đề: giữa lệnh GET và INCR có khoảng trống thời gian. Khi nhiều request đến đồng thời (burst), tất cả đều GET ra giá trị thấp, tất cả đều pass check, rồi tất cả đều INCR — counter vọt lên cao hơn limit mà không ai bị chặn:
# limit = 100, counter hiện tại = 99
# 10 request đến đồng thời:
# Tất cả 10 request đều GET → thấy 99 → pass check (99 < 100)
# Tất cả 10 request đều INCR → counter trở thành 109
# Kết quả: 10 request vượt limit được xử lý, counter = 109 (vượt limit 9 lần)
Giải pháp đúng là dùng INCR trực tiếp (trả về giá trị mới sau khi tăng) kết hợp với Lua script để đảm bảo toàn bộ logic check-and-increment là một thao tác atomic. Bài 32 đi chi tiết vào cả lý thuyết lẫn implementation với Lua và INCR/EXPIRE atomic pipeline.
Lộ Trình Module 3 & Tổng Kết
Lộ trình Module 3
- Bài 31 (bài này): tổng quan — vì sao cần, các layer, chiều limit, HTTP 429.
- Bài 32: atomicity — vì sao INCR đơn không đủ, Lua script và atomic pipeline.
- Bài 33: Fixed Window — implementation, boundary problem, khi nào đủ.
- Bài 34: Sliding Window Log — Sorted Set, ZREMRANGEBYSCORE, memory trade-off.
- Bài 35: Sliding Window Counter — weighted approximation, 2 counter.
- Bài 36: Token Bucket — nạp token theo thời gian, burst có kiểm soát.
- Bài 37: Leaky Bucket / GCRA — smooth traffic, cell rate algorithm.
- Bài 38: Distributed rate limiting — nhiều Redis node, consistency.
- Bài 39: Login protection thực chiến — brute force, credential stuffing.
- Bài 40: Quota per tier — free/pro/enterprise, feature-level quota.
Tổng kết bài 31
- Rate limiting giới hạn số request per client per window thời gian — bảo vệ system về cả bảo mật lẫn cost lẫn fairness.
- 4 lý do chính: chống abuse/attack, kiểm soát cost, fair usage, bảo vệ downstream service và database.
- Redis phù hợp vì in-memory (microsecond), atomic (INCR, Lua), TTL tự reset window, distributed (shared counter).
- Áp dụng nhiều layer: Edge → API Gateway → Application. Mỗi layer có vai trò khác nhau; không thay thế được nhau.
- Các chiều limit: per IP, per user, per API key, per endpoint, global, composite — thường kiểm tra nhiều chiều song song.
- HTTP 429 +
Retry-After+X-RateLimit-*headers là interface chuẩn với client. - 5 thuật toán (Fixed Window, Sliding Log, Sliding Counter, Token Bucket, Leaky Bucket) có trade-off khác nhau — chi tiết từ bài 33.
- Race condition GET-check-INCR là lỗi phổ biến; atomicity là yêu cầu bắt buộc — chủ đề bài 32.
Bài tiếp theo
Bài 32 phân tích atomicity: tại sao GET-rồi-INCR tách biệt là sai, cách dùng INCR kết hợp Lua script để đảm bảo check-and-increment là atomic, và các pattern atomic khác cho rate limiter.
