Xây dựng Rate Limiter với Redis
Bạn có biết?
Khi bạn gọi API của Shopee hay Facebook quá nhiều lần trong 1 phút, hệ thống sẽ trả về lỗi 429 Too Many Requests. Đó chính là Rate Limiter đang hoạt động. Với Redis, bạn có thể xây dựng rate limiter chỉ với vài dòng lệnh — nhanh, chính xác, và xử lý hàng triệu request mỗi giây.
Rate Limiter là gì?
Rate Limiter giới hạn số lần một user hoặc IP có thể thực hiện hành động trong khoảng thời gian nhất định. Mục tiêu:
- Bảo vệ API — Chống abuse, DDoS
- Quản lý tài nguyên — Tránh quá tải server
- Fair usage — Đảm bảo công bằng giữa các user
- Giảm chi phí — Giới hạn call tới API trả phí (OpenAI, Stripe…)
Các thuật toán phổ biến
| Thuật toán | Cơ chế | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Fixed Window | Đếm trong khung thời gian cố định | Đơn giản, dễ implement | Burst ở biên cửa sổ |
| Sliding Window Log | Lưu timestamp từng request | Chính xác nhất | Tốn bộ nhớ |
| Sliding Window Counter | Kết hợp Fixed Window + tỷ lệ | Cân bằng hiệu suất | Không hoàn toàn chính xác |
| Token Bucket | Thêm token theo thời gian | Cho phép burst | Phức tạp hơn |
Bài này sẽ implement cả 3 cách phổ biến nhất với Redis.
Cách 1: Fixed Window Counter
Đơn giản nhất — đếm số request trong khung thời gian cố định (ví dụ: 1 phút).
Cơ chế
- Key:
rate:user:{id}:{window} - Mỗi request → INCR counter
- Nếu counter > limit → từ chối
- Key tự hết hạn sau window size
Redis CLI
# User 123 gửi request đầu tiên trong phút hiện tại
127.0.0.1:6379> INCR rate:user:123:1711785600
(integer) 1
# Đặt TTL lần đầu (chỉ set 1 lần)
127.0.0.1:6379> EXPIRE rate:user:123:1711785600 60
(integer) 1
# Request thứ 2, 3, 4...
127.0.0.1:6379> INCR rate:user:123:1711785600
(integer) 2
# Kiểm tra số request hiện tại
127.0.0.1:6379> GET rate:user:123:1711785600
"2"
# Nếu vượt quá 100 request/phút → trả lỗi 429
Python Implementation
import redis
import time
r = redis.Redis(host='localhost', port=6379)
def is_allowed_fixed_window(user_id, limit=100, window=60):
current_window = int(time.time()) // window
key = f"rate:user:{user_id}:{current_window}"
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
return count <= limit
# Sử dụng
if is_allowed_fixed_window("user_123", limit=100, window=60):
print("Request được phép")
else:
print("429 Too Many Requests")
Vấn đề của Fixed Window
Giới hạn 100 request/phút. User gửi 100 request ở giây cuối của phút 1, rồi 100 request ở giây đầu của phút 2 → 200 request trong 2 giây! Đây gọi là burst ở biên cửa sổ.
Cách 2: Sliding Window Log
Lưu timestamp của từng request. Khi có request mới, loại bỏ các timestamp cũ hơn window.
Redis CLI
# User 123 gửi request lúc timestamp 1711785600
127.0.0.1:6379> ZADD rate:log:user:123 1711785600 1711785600
(integer) 1
# Request thứ 2 lúc 1711785605
127.0.0.1:6379> ZADD rate:log:user:123 1711785605 1711785605
(integer) 1
# Xóa timestamp cũ hơn 60 giây
127.0.0.1:6379> ZREMRANGEBYSCORE rate:log:user:123 0 1711785540
(integer) 0
# Đếm số request trong window
127.0.0.1:6379> ZCARD rate:log:user:123
(integer) 2
# Đặt TTL
127.0.0.1:6379> EXPIRE rate:log:user:123 60
(integer) 1
Python Implementation
import redis
import time
r = redis.Redis(host='localhost', port=6379)
def is_allowed_sliding_window(user_id, limit=100, window=60):
key = f"rate:log:{user_id}"
now = time.time()
window_start = now - window
pipe = r.pipeline()
# Xóa request cũ
pipe.zremrangebyscore(key, 0, window_start)
# Thêm request hiện tại
pipe.zadd(key, {str(now): now})
# Đếm request trong window
pipe.zcard(key)
# Đặt TTL
pipe.expire(key, window)
_, _, count, _ = pipe.execute()
return count <= limit
Ưu điểm
Chính xác 100% — không có burst ở biên cửa sổ. Nhưng tốn bộ nhớ vì lưu từng timestamp.
Cách 3: Token Bucket
Mỗi user có một "bucket" chứa token. Token được thêm vào theo tốc độ cố định. Mỗi request tiêu tốn 1 token. Hết token → từ chối.
Cơ chế
- Bucket có sức chứa tối đa (burst capacity)
- Token được thêm theo rate (ví dụ: 10 token/giây)
- Request lấy 1 token từ bucket
- Bucket đầy → cho phép burst ngắn hạn
Redis CLI
# Lưu trạng thái bucket bằng Hash
127.0.0.1:6379> HSET rate:bucket:user:123 tokens 10 last_refill 1711785600
(integer) 2
# Khi có request → kiểm tra và cập nhật
# (Thường dùng Lua script để đảm bảo atomic)
# Lua script:
eval "
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- tokens per second
local capacity = tonumber(ARGV[2]) -- max tokens
local now = tonumber(ARGV[3]) -- current time
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- Tính token mới
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1 -- allowed
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 0 -- denied
end
" 1 rate:bucket:user:123 10 20 1711785610
Python Implementation
import redis
import time
r = redis.Redis(host='localhost', port=6379)
LUA_SCRIPT = """
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 0
end
"""
token_bucket = r.register_script(LUA_SCRIPT)
def is_allowed_token_bucket(user_id, rate=10, capacity=20):
key = f"rate:bucket:{user_id}"
now = time.time()
result = token_bucket(keys=[key], args=[rate, capacity, now])
return result == 1
# Sử dụng
for i in range(25):
allowed = is_allowed_token_bucket("user_123", rate=10, capacity=20)
print(f"Request {i+1}: {'✅ Allowed' if allowed else '❌ Denied'}")
So sánh 3 thuật toán
| Tiêu chí | Fixed Window | Sliding Window Log | Token Bucket |
|---|---|---|---|
| Độ chính xác | Trung bình | Cao nhất | Cao |
| Bộ nhớ | Ít nhất | Nhiều nhất | Trung bình |
| Performance | Nhanh nhất | Chậm nhất | Nhanh |
| Cho phép burst | Không kiểm soát | Không | Có kiểm soát |
| Phù hợp | API đơn giản | Cần chính xác | Production |
Use Cases thực tế
1. API Gateway
# Giới hạn 1000 request/giờ cho mỗi API key
def check_api_limit(api_key):
return is_allowed_token_bucket(
f"api:{api_key}",
rate=1000/3600, # ~0.28 token/giây
capacity=1000
)
2. Login Protection
# Giới hạn 5 lần đăng nhập sai trong 15 phút
def check_login_attempts(ip_address):
return is_allowed_fixed_window(
f"login:{ip_address}",
limit=5,
window=900 # 15 phút
)
3. Per-user API Quota
# Free tier: 100 request/ngày
# Pro tier: 10000 request/ngày
def check_user_quota(user_id, tier):
limits = {"free": 100, "pro": 10000}
return is_allowed_fixed_window(
f"quota:{user_id}",
limit=limits[tier],
window=86400 # 24 giờ
)
Best Practices
- Dùng Lua script — Đảm bảo atomic khi check + update
- Trả header Rate Limit —
X-RateLimit-Remaining,X-RateLimit-Reset - Phân tầng giới hạn — Global → Per-IP → Per-User
- Graceful degradation — Queue request thay vì reject ngay
- Monitor rate limit events — Log khi user bị limit để phát hiện abuse
- Redis Cluster — Nếu traffic lớn, dùng Cluster để phân tán load

Bước tiếp theo
Bạn đã xây dựng được Rate Limiter hoàn chỉnh! Tiếp theo, chúng ta sẽ tìm hiểu cách xây dựng Session Store — quản lý phiên đăng nhập user với Redis.