Danh sách bài viết

Bài 31: Vì Sao Cần Rate Limiting — Abuse, Cost, Fair Usage

Rate limiting giới hạn số request một client được thực hiện trong một khoảng thời gian xác định — ví dụ "100 req/phút per user" hay "10 lần thử đăng nhập/giờ per IP". Bài này đặt nền cho Module 3 bằng cách phân tích 4 lý do cần rate limiting (chống abuse/attack, kiểm soát cost, fair usage, bảo vệ downstream), lý do Redis là lựa chọn phù hợp, vị trí áp dụng theo layer, các chiều giới hạn, HTTP 429 và response headers chuẩn, preview 5 thuật toán sẽ đào sâu từ bài 33, cùng trade-off business và các trường hợp chưa — và bắt buộc — cần rate limit.

28/05/2026
14 phút đọc
0 lượt xem
1

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-*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.
2

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út per 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ày per số điện thoại — kiểm soát cost SMS.
  • 50 request/giây per 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).
3

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.
4

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:

  1. 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ệ.
  2. 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.
  3. 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: INCR trê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 EXPIRE cho 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.

5

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.
6

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.

7

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-LimitX-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.

8

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ể.

9

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.
10

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.

11

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 GETINCR 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.

12

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.

Tham khảo