# 🚦 Rate limiting theo IP và theo user

By Better Wise

## Từ một hóa đơn 360 USD trong một giờ

Mình từng mở endpoint `/api/chat` gọi API AI bên thứ ba mà quên đặt giới hạn. Một script lặp vô hạn gõ 50 request mỗi giây, đúng một giờ sau mình có 180.000 request và hóa đơn khoảng 360 USD cho một tính năng chưa có người dùng thật. Bài này là lớp gạch đầu tiên để chuyện đó không lặp lại: rate limiting. Bạn đã biết viết API đơn giản và đã từng gọi API bên thứ ba, vậy là đủ để bắt đầu.

Sau bài này, bạn sẽ:

- Phân biệt được rate limiting theo IP, theo user và theo API key, và biết khi nào dùng cái nào.
- Cấu hình được một rule rate limiting cơ bản với bộ đếm dùng chung.
- Trả về response 429 đúng chuẩn kèm header để client biết khi nào được thử lại.
- Ghép hai lớp giới hạn cho project của khóa học.

## Vì sao phải đặt giới hạn trước khi mở cửa

Một API không giới hạn là một API cho phép người lạ tiêu tiền của bạn ở tốc độ họ chọn. Chi phí không tăng theo số người dùng thật, nó tăng theo số request, và request thì có thể đến từ một vòng lặp `while(true)`, một lần retry cấu hình sai, hoặc một bot quét endpoint.

Rule of thumb của mình: **đặt giới hạn trước khi mở cửa**. Một con số xấu vẫn tốt hơn không có con số nào.

## Chọn khóa giới hạn: IP, user hay API key

Câu hỏi đầu tiên không phải "dùng thuật toán nào", mà là "đếm theo cái gì".

| Khóa | Dùng khi | Điểm mạnh | Điểm yếu |
| --- | --- | --- | --- |
| IP | Endpoint công khai, khách chưa đăng nhập | Rẻ, áp được trước khi biết danh tính | Nhiều người chung một IP (NAT, văn phòng) bị chặn oan; kẻ tấn công xoay IP, dùng proxy |
| User ID | Người dùng đã đăng nhập | Chia công bằng theo tài khoản, gắn được với quota | Phải xác thực trước khi đếm; tạo nhiều account là cách né |
| API key | Đối tác gọi server-to-server | Gắn với hợp đồng, dễ nâng hạn mức theo gói | Key lộ là mất kiểm soát, phải có cơ chế thu hồi |

Với đa số sản phẩm, câu trả lời là dùng cả hai: IP làm lớp ngoài, user hoặc API key làm lớp trong. Lớp IP chặn được đợt quét vô danh; lớp user đảm bảo một tài khoản không nuốt hết hạn mức của người khác.

> **Note:** 
> IP trong thực tế thường không phải IP của client. Nếu API của bạn nằm sau proxy hoặc CDN, hãy đọc `X-Forwarded-For` một cách cẩn thận và chỉ tin header này từ proxy của bạn, nếu không client sẽ tự đặt header để né giới hạn.

**Bài tập 1 (theo project):** Endpoint `POST /api/summarize` của bạn cho khách vãng lai dùng thử, khách đã đăng nhập dùng nhiều hơn. Bạn chọn khóa nào cho rule đầu tiên?

**Xem gợi ý**

Áp hai rule: khóa IP cho khách vãng lai với hạn mức thấp (ví dụ 5 request/phút), khóa user ID cho khách đã đăng nhập với hạn mức cao hơn (ví dụ 30 request/phút). Đừng chỉ dùng IP cho tất cả, vì cả văn phòng dùng chung IP sẽ chặn lẫn nhau, và cũng đừng chỉ dùng user, vì khách vãng lai chưa có user để đếm.

## Cấu hình một rule rate limiting cơ bản

Có ba cách đếm bạn sẽ gặp:

- **Fixed window:** đếm trong một khung cố định (mỗi phút). Đơn giản nhưng có kẽ hở ở ranh giới: client gửi hết hạn mức ở giây 59 và gửi tiếp ở giây 61, tức gấp đôi trong hai giây.
- **Sliding window:** nhìn lại đúng N giây gần nhất, mượt hơn, tốn bộ nhớ hơn.
- **Token bucket:** một xô chứa token, nạp lại đều theo thời gian, mỗi request lấy một token. Cho phép burst ngắn nhưng giữ tốc độ trung bình.

Với người mới, fixed window là đủ để bắt đầu, miễn là bạn biết kẽ hở của nó. Điều quan trọng hơn thuật toán là bộ đếm phải dùng chung giữa các instance. Đếm trong bộ nhớ của từng process thì khi scale lên 3 instance, hạn mức thật của bạn thành 3 lần.

```js
// Express + Redis: 60 request/phút cho mỗi user, fallback theo IP
app.use(async (req, res, next) => {
  const key = `rl:${req.user?.id ?? req.ip}`;
  const limit = 60;
  const windowSec = 60;

  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSec);

  const ttl = await redis.ttl(key);
  res.set("RateLimit-Limit", limit);
  res.set("RateLimit-Remaining", Math.max(0, limit - count));
  res.set("RateLimit-Reset", ttl);

  if (count > limit) {
    res.set("Retry-After", ttl);
    return res.status(429).json({
      error: "rate_limited",
      message: "Bạn gửi quá nhiều request, vui lòng thử lại sau.",
      retry_after: ttl
    });
  }
  next();
});
```

Hai chi tiết dễ sai: đặt `expire` ngay lần `incr` đầu tiên (nếu không key sống mãi và người dùng bị khóa vĩnh viễn), và luôn set `expire` cho mọi key, kể cả key của IP lạ.

## Trả 429 đúng chuẩn để client biết đường lùi

Khi vượt giới hạn, mã đúng là **429 Too Many Requests**. Đừng trả 200 kèm body báo lỗi, vì SDK, retry logic và dashboard của bạn sẽ không nhận ra đó là lỗi, và bạn mất luôn tín hiệu cảnh báo.

| Header | Ý nghĩa |
| --- | --- |
| `RateLimit-Limit` | Hạn mức trong cửa sổ hiện tại |
| `RateLimit-Remaining` | Số request còn lại |
| `RateLimit-Reset` | Số giây đến khi cửa sổ reset |
| `Retry-After` | Số giây client nên chờ trước khi thử lại |

`Retry-After` là header quan trọng nhất với client tử tế: có nó, họ chờ đúng; không có nó, họ retry ngay và làm tình hình tệ hơn.

**Bài tập 2:** Một user vừa vượt hạn mức 60 request/phút, cửa sổ còn 23 giây. Viết status và các header chính.

**Xem gợi ý**

Status `429`, kèm `Retry-After: 23`, `RateLimit-Limit: 60`, `RateLimit-Remaining: 0`, `RateLimit-Reset: 23`, và body JSON có mã lỗi ổn định (ví dụ `rate_limited`) để client xử lý theo mã chứ không parse câu thông báo.

## Ghép hai lớp: IP ở ngoài, user ở trong

Thứ tự kiểm tra nên phản ánh chi phí: cái gì rẻ thì kiểm trước, cái gì tốn tiền thì kiểm sau, và chặn càng sớm càng tốt.

```mermaid
flowchart TD
    Req["📥 Request"] --> IpCheck{"🛡️ IP limit"}
    IpCheck -->|Vượt| R429["🚫 429 Too Many Requests"]
    IpCheck -->|OK| AuthCheck{"👤 Đã đăng nhập?"}
    AuthCheck -->|Có| UserCheck{"🔑 User limit"}
    AuthCheck -->|Không| Handler["⚙️ Xử lý API"]
    UserCheck -->|Vượt| R429
    UserCheck -->|OK| Handler
```

Hai lớp này bổ sung cho nhau chứ không thay thế nhau. IP chặn đợt quét trước khi chạm tới database hay API AI; user đảm bảo một tài khoản không kéo cả hệ thống xuống. Trong project của khóa học, bạn sẽ đặt cả hai ngay trước route gọi dịch vụ bên ngoài, vì đó là chỗ mỗi request đều thành tiền.

## Những chỗ rule rate limit thường bị hở

- **Quên endpoint tốn tiền nhất.** Hãy đặt hạn mức chặt nhất cho route gọi API AI, không phải route `/health`.
- **Đếm trong RAM từng instance.** Khi scale, hạn mức thật nhân lên theo số instance. Dùng Redis hoặc để API gateway đếm.
- **Không loại trừ nhau.** Đừng để một user mở 20 tab và đếm mỗi tab một lần; khóa phải là user, không phải session.
- **Chặn mà không báo.** Trả 429 kèm header rõ ràng, và ghi log theo khóa để biết ai đang bị chặn nhiều.
- **Đặt hạn mức quá chặt ngay ngày đầu.** Bạn sẽ chặn chính người dùng thật. Bắt đầu rộng, siết dần khi có số liệu.
- **Bỏ quên retry.** Client tự retry không có backoff sẽ tạo ra đợt sóng mới. `Retry-After` là cách rẻ nhất để dạy client chờ.

## Chốt lại và bước sang quota

- Rate limiting giới hạn **tốc độ**, tính theo cửa sổ thời gian; đừng nhầm với quota, thứ giới hạn **tổng lượng** theo kỳ.
- Chọn khóa theo danh tính bạn đang có: IP cho khách vãng lai, user ID cho người đã đăng nhập, API key cho đối tác.
- Luôn trả 429 kèm `Retry-After` và các header `RateLimit-*`, và luôn đếm bằng store dùng chung.
- Đặt IP ở lớp ngoài, user ở lớp trong, chặn trước khi request chạm tới chỗ tốn tiền.

Bài sau, chúng ta đi tiếp phần còn thiếu: quota theo user và validate input thật sớm, để request rác bị chặn trước cả khi nó kịp tốn một đồng nào.

Rate limiting theo IP dùng _____ làm khóa, phù hợp cho khách chưa đăng nhập.
Khi client vượt giới hạn, API nên trả về mã _____ kèm header _____.
Header _____ cho biết còn bao nhiêu request trong cửa sổ hiện tại.
Cách ghép hai lớp an toàn là IP ở lớp _____ và user ở lớp _____.

Front: Rate limiting khác quota ở chỗ nào?
Back: Rate limiting giới hạn tốc độ theo cửa sổ thời gian (60 request/phút), quota giới hạn tổng lượng theo kỳ (10.000 request/tháng).

Front: Vì sao chỉ rate limit theo IP là chưa đủ?
Back: Nhiều người dùng chung một IP sẽ bị chặn oan, còn kẻ tấn công có thể xoay IP hoặc dùng proxy.

Front: Khi nào nên dùng user ID làm khóa giới hạn?
Back: Khi người dùng đã đăng nhập, để chia hạn mức công bằng theo từng tài khoản.

Front: Khi nào nên dùng API key làm khóa giới hạn?
Back: Khi đối tác gọi server-to-server, mỗi key gắn với một mức hạn mức theo hợp đồng.

Front: Mã HTTP chuẩn trả về khi client vượt giới hạn là gì?
Back: 429 Too Many Requests.

Front: Header nào cho client biết nên chờ bao lâu trước khi thử lại?
Back: Retry-After, tính bằng số giây hoặc một mốc thời gian HTTP.

Front: Ba header chuẩn mô tả trạng thái giới hạn?
Back: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset.

Front: Fixed window có nhược điểm gì?
Back: Bùng nổ ở ranh giới cửa sổ: client có thể gửi gấp đôi hạn mức quanh mốc chuyển cửa sổ.

Front: Token bucket hoạt động thế nào?
Back: Một xô chứa token nạp lại đều theo thời gian, mỗi request lấy một token, cho phép burst ngắn nhưng giữ tốc độ trung bình.

Front: Thứ tự ghép hai lớp giới hạn nên là gì?
Back: IP ở lớp ngoài, user hoặc API key ở lớp trong.

Front: Vì sao không nên trả 200 kèm body báo lỗi khi vượt giới hạn?
Back: SDK, retry logic và monitoring sẽ không nhận ra lỗi; 429 giúp client và cảnh báo xử lý đúng.

Front: Endpoint nào nên có hạn mức chặt nhất?
Back: Endpoint tốn tài nguyên hoặc tốn tiền, ví dụ route gọi API AI bên thứ ba.

Front: Vì sao không nên đếm rate limit trong bộ nhớ từng instance?
Back: Khi scale lên nhiều instance, hạn mức thật nhân lên theo số instance; cần store dùng chung như Redis hoặc API gateway.

---

## Contents

Part of [🛡️ Chống lạm dụng API: rate limit, bot và chi phí AI](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai.md)

- **🚦 Kiểm soát lưu lượng và chống lạm dụng API**
  - [🚦 Rate limiting theo IP và theo user](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/rate-limiting-theo-ip-va-theo-user.md) — this lesson
  - [📊 Quota theo user và validate input sớm để tiết kiệm tài nguyên](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/quota-theo-user-va-validate-input-som.md) (sign-in required)
  - [🤖 Captcha và chống bot cơ bản](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/captcha-va-chong-bot-co-ban.md)
  - [✅ Checkpoint: rate limiting, quota và chống bot](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/checkpoint-rate-limiting-quota-chong-bot.md)
- **💰 Kiểm soát chi phí API AI bên thứ ba**
  - [🧮 Đo và dự phóng chi phí khi gọi API AI bên thứ ba](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/do-va-du-phong-chi-phi-api-ai.md)
  - [🔔 Đặt ngân sách và cảnh báo khi vượt](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/dat-ngan-sach-va-canh-bao-khi-vuot.md)
  - [🛠️ Project: dựng lớp bảo vệ và bảng theo dõi chi phí cho API](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/project-lop-bao-ve-va-bang-theo-doi-chi-phi-api.md) (premium content)
  - [✅ Checkpoint: chi phí, ngân sách và cảnh báo](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/checkpoint-chi-phi-ngan-sach-va-canh-bao.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/chong-lam-dung-api-rate-limit-bot-va-chi-phi-ai/cheatsheet.md) (premium content)
