# 📊 Quota theo user và validate input sớm để tiết kiệm tài nguyên

By Better Wise

> Sign in to view the full content of this lesson.

## Vì sao rate limiting vẫn chưa đủ

Bài trước bạn đã chặn được một IP gọi 500 request trong 10 giây. Tốt. Nhưng rate limiting chỉ giới hạn **tốc độ**, không giới hạn **tổng lượng**. Một script kiên nhẫn gọi đúng 9 request mỗi phút, chạy suốt 30 ngày, vẫn lọt qua mọi cửa sổ rate limit và vẫn đốt sạch ngân sách của bạn. Tôi từng nhìn một hóa đơn tăng gấp bốn lần chỉ vì lý do đó: không có ai tấn công, chỉ là không có ai đặt trần.

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

- Phân biệt được rate limiting và quota, và biết khi nào cần cả hai.
- Thiết kế quota theo user và theo gói (free, pro, team) bằng Redis.
- Đặt validate input ngay tại cửa vào để request rác không tốn CPU, database hay lượt gọi API AI.
- Đọc được log để biết lớp bảo vệ của mình có thật sự chạy.

## Quota theo user và theo gói

Quota là **tổng mức sử dụng được phép trong một chu kỳ dài**, ví dụ 100 lượt gọi AI mỗi tháng cho gói free, 5.000 lượt cho gói pro.

| | Rate limiting | Quota |
| --- | --- | --- |
| Đo cái gì | Tốc độ (request/phút) | Tổng lượng (lượt/tháng) |
| Cửa sổ | Vài giây đến vài phút | Ngày, tháng, hoặc theo chu kỳ gói |
| Chống | Burst, spam tức thời | Lạm dụng tổng thể, chi phí leo thang |
| Phạm vi | IP, user, API key | User, theo gói |

Ví dụ chi phí cụ thể: giả sử mỗi lượt gọi model trả phí tốn khoảng 0,002 USD. Gói free 100 lượt tốn 0,2 USD, chấp nhận được. Không có quota, một user có thể gọi 200.000 lượt, tức khoảng 400 USD, từ một tài khoản miễn phí.

### Đếm quota bằng Redis

Đừng đếm bằng `COUNT(*)` trên bảng log mỗi request. Bảng lớn dần, query chậm dần, và bạn vừa tốn database vừa tốn tiền. Dùng bộ đếm Redis với key theo chu kỳ:

```js
const key = `quota:${userId}:${monthKey}`; // quota:u_123:2025-06
const used = await redis.incr(key);
if (used === 1) {
  await redis.expire(key, secondsUntilEndOfMonth());
}
if (used > plan.limit) {
  return res.status(429).json({
    error: "quota_exceeded",
    remaining: 0,
    resetAt: endOfMonthIso(),
  });
}
```

Ba điểm quan trọng:

1. `INCR` là **atomic**, nên 10 request song song không cùng đọc ra `used = 98`.
2. Key phải gắn chu kỳ (`YYYY-MM`), nếu không bộ đếm sẽ chạy mãi.
3. Trả về `remaining` và `resetAt` để client biết đường xử lý, thay vì đoán mò.

> **Warning:** 
> Nếu bạn đọc quota bằng `GET` rồi mới `SET used + 1`, đó là race condition. Hai request đồng thời cùng thấy `98`, cùng ghi `99`, và user được dùng 101 lượt. Luôn dùng `INCR` hoặc một Lua script atomic.

**Thực hành:** User gói free (100 lượt/tháng) đã dùng 98 lượt và mở 5 tab gọi song song. Với code `GET` rồi `SET`, điều gì xảy ra và sửa thế nào?

**Xem gợi ý**

Cả 5 request đều đọc `98`, đều thấy còn quota, đều ghi `99`, nên user dùng tới 103 lượt. Sửa bằng cách `INCR` trước rồi mới so với `plan.limit`, hoặc gộp kiểm tra và tăng trong một Lua script. Chấp nhận đánh đổi: request bị chặn vẫn bị tính vào bộ đếm, nên đặt thêm rate limit để tránh việc gọi liên tục vào endpoint đã hết quota.

**Thực hành:** Bạn có ba gói: free 100, pro 5.000, team 20.000 lượt/tháng. Hãy viết ra key Redis và hai header bạn sẽ trả về cho client.

**Xem gợi ý**

Key: `quota:{user_id}:{YYYY-MM}` (thêm tên gói vào giá trị, không cần vào key, vì gói có thể đổi giữa chu kỳ). Header: `X-Quota-Limit`, `X-Quota-Remaining`, và nên có thêm `X-Quota-Reset`. Khi gói được nâng cấp, chỉ cần đổi `plan.limit` khi so sánh, không phải chuyển key.

## Validate input sớm: chặn trước khi tốn tiền

Mỗi request đi qua càng nhiều bước đắt tiền thì một payload sai càng gây thiệt hại lớn. Nguyên tắc: **kiểm tra cái rẻ nhất trước**.

```mermaid
flowchart TD
  A["📥<br/>Request"] --> B["📏<br/>Content-Length"]
  B --> C["🔐<br/>Auth"]
  C --> D["⏱️<br/>Rate limit"]
  D --> E["🧹<br/>Schema"]
  E --> F["📊<br/>Quota"]
  F --> G["🤖<br/>Gọi API AI"]
```

Các bước kiểm tra, xếp từ rẻ đến đắt:

1. **Kích thước payload.** Chặn bằng `Content-Length` ở middleware, trước khi body parser đọc và parse JSON. Một body 5 MB bị từ chối ở đây không tốn CPU parse, không tốn bộ nhớ.
2. **Kiểu dữ liệu và độ dài từng field.** `prompt` phải là string, dưới 4.000 ký tự, `language` phải nằm trong danh sách hỗ trợ.
3. **Business rule.** File phải là PDF, ảnh phải dưới 2 MB, ngôn ngữ phải có trong gói.
4. **Chỉ khi tất cả đều qua** mới chạm database và gọi API AI.

```js
const MAX_PROMPT = 4000;
if (typeof body.prompt !== "string") return badRequest("prompt_required");
if (body.prompt.length > MAX_PROMPT) return badRequest("prompt_too_long");
if (!SUPPORTED_LANGS.includes(body.language)) return badRequest("unsupported_language");
```

Một request có `prompt` dài 2 MB đi thẳng lên model là bạn đã trả tiền cho token, rồi mới nhận về lỗi. Chặn ở dòng `length > MAX_PROMPT` tốn gần như bằng không.

> **Tip:** 
> Request bị validate lỗi thì **không trừ quota**, vì bạn chưa gọi upstream và không tốn đồng nào. Nhưng vẫn phải rate limit, nếu không kẻ xấu có thể spam endpoint validate miễn phí để dò lỗi.

**Thực hành:** Endpoint `POST /summarize` hiện parse JSON, kiểm tra `text.length > 0`, rồi gọi AI. Nêu hai chỗ chặn sớm hơn và mỗi chỗ tiết kiệm được gì.

**Xem gợi ý**

Thứ nhất, chặn `Content-Length` lớn hơn 20 KB ngay trong middleware để không parse JSON khổng lồ (tiết kiệm CPU và RAM). Thứ hai, chặn `text.length > 8000` trước khi gọi AI (tiết kiệm token và tiền). Có thể thêm bước chuẩn hóa: cắt khoảng trắng thừa trước khi đếm ký tự, để user không bị chặn oan.

## Những cái bẫy khiến quota vẫn bị lách

- **Đếm quota bằng database.** Query `COUNT(*)` mỗi request là cách nhanh nhất để tự tấn công database của mình. Dùng bộ đếm Redis.
- **Không atomic.** `GET` rồi `SET` luôn sai dưới tải song song. Dùng `INCR`.
- **Quên `EXPIRE`.** Key quota không bao giờ hết hạn nghĩa là user gói free bị khóa vĩnh viễn sau tháng đầu.
- **Validate sau khi đã gọi upstream.** Đây là lỗi đắt nhất và cũng phổ biến nhất.
- **Mặc định cho phép.** User mới nên bắt đầu ở gói thấp nhất với quota rõ ràng, nâng cấp có kiểm soát. Đặt giới hạn trước khi mở cửa.
- **Không đo số request bị chặn.** Nếu log của bạn ghi 0 request bị chặn trong nhiều ngày, rất có thể lớp bảo vệ chưa được gắn vào route, chứ không phải người dùng ngoan.

## Chốt lại và bước tiếp theo

Điền nhanh để tự kiểm tra: quota nên được đếm bằng bộ đếm _____ trong Redis, thường dùng lệnh _____. Khi user vượt quota, trả về HTTP status _____ kèm header cho biết số lượt còn lại. Bước kiểm tra rẻ nhất và nên chạy đầu tiên là _____. Request bị validate lỗi thì _____, nhưng vẫn phải rate limit.

Front: Rate limiting giới hạn điều gì?
Back: Tốc độ request trong một cửa sổ ngắn, ví dụ 10 request mỗi phút.

Front: Quota giới hạn điều gì?
Back: Tổng lượng sử dụng trong một chu kỳ dài, theo user hoặc theo gói.

Front: Vì sao đã có rate limiting vẫn cần quota?
Back: Vì request chậm rãi, đều đặn vẫn tiêu hết ngân sách nếu không có trần tổng.

Front: Lệnh Redis nào giúp tăng bộ đếm quota an toàn khi có request song song?
Back: INCR, vì nó atomic, kèm EXPIRE khi tạo key lần đầu.

Front: Key Redis cho quota theo tháng nên có dạng nào?
Back: quota:{user_id}:{YYYY-MM}

Front: User vượt quota thì trả HTTP status nào?
Back: 429 Too Many Requests, kèm header remaining và resetAt.

Front: Vì sao kiểm tra Content-Length trước khi parse JSON?
Back: Để chặn payload quá lớn mà không tốn CPU và bộ nhớ cho việc parse.

Front: Request bị validate lỗi có nên trừ quota không?
Back: Không, vì chưa gọi upstream và không tốn chi phí, nhưng vẫn phải rate limit.

Front: Thứ tự xử lý request tiết kiệm nhất là gì?
Back: Việc rẻ trước: kích thước, auth, rate limit, schema, quota, rồi mới gọi API AI.

Front: Vì sao không đếm quota bằng COUNT(*) trên database?
Back: Query chậm dần khi bảng lớn và tốn tài nguyên database cho mỗi request.

Front: Dấu hiệu lớp giới hạn của bạn chưa hoạt động?
Back: Log ghi 0 request bị chặn trong nhiều ngày liên tiếp.

Front: Validate input sớm tiết kiệm những gì?
Back: CPU parse, query database, và lượt gọi API AI tốn tiền.

Front: Quota mặc định cho user mới nên đặt thế nào?
Back: Gói thấp nhất, deny by default, nâng cấp có kiểm soát.

Bạn giờ đã có hai lớp: chặn theo tốc độ và chặn theo tổng lượng. Bài tiếp theo xử lý phần khó chịu hơn, những request trông hoàn toàn hợp lệ về hình thức nhưng do bot gửi, bằng captcha và vài dấu hiệu nhận diện bot cơ bản.

---

## 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)
  - [📊 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) — this lesson
  - [🤖 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)
