ในโลกของการพัฒนา AI application ในปัจจุบัน การจัดการ request ที่มีปริมาณสูงเป็นสิ่งจำเป็นอย่างยิ่ง จากประสบการณ์ตรงของผมในการสร้างระบบที่รองรับผู้ใช้หลายหมื่นรายต่อวัน พบว่าการ implement rate limiting ที่ไม่เหมาะสมนำไปสู่ปัญหาร้ายแรงได้ ไม่ว่าจะเป็นค่าใช้จ่ายที่พุ่งสูงเกินควบคุม หรือ service ล่มจากการโจมตีแบบ DoS โดยไม่ตั้งใจ
บทความนี้จะพาคุณเข้าใจ architecture ของระบบ rate limiting ตั้งแต่พื้นฐานจนถึง advanced technique พร้อมโค้ด production-ready ที่สามารถนำไปใช้ได้จริง รวมถึงการ integrate กับ HolySheep AI ซึ่งมีความโดดเด่นเรื่อง latency เฉลี่ยต่ำกว่า 50ms และราคาประหยัดกว่า 85% เมื่อเทียบกับ provider อื่น
ทำไมต้องมี Rate Limiting?
ก่อนจะลงลึกใน technical detail มาทำความเข้าใจปัญหาที่ rate limiting แก้ไข
ปัญหาด้านความปลอดภัย
- การโจมตีแบบ brute force — ผู้ไม่หวังดีอาจพยายามเดาคีย์ API ด้วยจำนวน request มหาศาล
- Avoid resource exhaustion — ป้องกันไม่ให้ single client ใช้ทรัพยากรทั้งหมดของระบบ
- ค่าใช้จ่ายที่ไม่คาดคิด — หากไม่มีการควบคุม ค่าใช้จ่าย API อาจพุ่งสูงจนส่งผลกระทบต่อธุรกิจ
ปัญหาด้านประสิทธิภาพ
- Cold start latency — การ request พร้อมกันจำนวนมากทำให้เกิด bottleneck
- Database contention — การเขียน log และ metrics พร้อมกันสร้าง lock contention
- Downstream service degradation — upstream service ที่ไม่มีการควบคุมอาจทำให้ provider API ล่ม
Rate Limiting Algorithm ที่นิยมใช้
1. Token Bucket Algorithm
เป็น algorithm ที่เหมาะกับ use case ที่ต้องการ allow burst traffic แต่ยังควบคุม average rate ได้ หลักการคือมี bucket ที่บรรจุ token จำนวนหนึ่ง แต่ละ request จะใช้ token 1 token และ token จะถูกเติมในอัตราคงที่
"""
Token Bucket Rate Limiter Implementation
Suitable for burst traffic handling
"""
import time
import threading
from dataclasses import dataclass, field
from typing import Dict, Optional
import asyncio
@dataclass
class TokenBucket:
"""Token Bucket implementation with thread-safe operations"""
capacity: int
refill_rate: float # tokens per second
tokens: float = field(init=False)
last_refill: float = field(init=False)
lock: threading.Lock = field(default_factory=threading.Lock)
def __post_init__(self):
self.tokens = float(self.capacity)
self.last_refill = time.monotonic()
def _refill(self) -> None:
"""Refill tokens based on elapsed time"""
now = time.monotonic()
elapsed = now - self.last_refill
self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
self.last_refill = now
def consume(self, tokens: int = 1) -> bool:
"""
Attempt to consume tokens from bucket
Returns True if successful, False if rate limited
"""
with self.lock:
self._refill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
def available_tokens(self) -> float:
"""Get current available tokens"""
with self.lock:
self._refill()
return self.tokens
class RateLimiter:
"""
Production-grade rate limiter supporting:
- Per-client rate limiting
- Multiple buckets per client (different endpoints)
- Automatic cleanup of stale clients
"""
def __init__(
self,
requests_per_second: float = 10,
burst_size: int = 20,
max_clients: int = 100000,
cleanup_interval: int = 300
):
self.buckets: Dict[str, TokenBucket] = {}
self.rps = requests_per_second
self.burst = burst_size
self.max_clients = max_clients
self.lock = threading.RLock()
# Cleanup stale clients periodically
self._cleanup_task = threading.Thread(
target=self._cleanup_loop,
args=(cleanup_interval,),
daemon=True
)
self._cleanup_task.start()
def _cleanup_loop(self, interval: int) -> None:
"""Remove stale buckets to prevent memory leak"""
while True:
time.sleep(interval)
self._cleanup_stale()
def _cleanup_stale(self) -> None:
"""Remove buckets with no activity in last hour"""
cutoff = time.monotonic() - 3600
with self.lock:
stale = [
k for k, v in self.buckets.items()
if v.last_refill < cutoff
]
for k in stale:
del self.buckets[k]
if len(stale) > 0:
print(f"[RateLimiter] Cleaned up {len(stale)} stale clients")
def check_rate_limit(self, client_id: str) -> tuple[bool, dict]:
"""
Check if request should be allowed
Returns (allowed, headers_dict)
"""
with self.lock:
if client_id not in self.buckets:
if len(self.buckets) >= self.max_clients:
# Remove oldest client when at capacity
oldest = min(self.buckets.items(), key=lambda x: x[1].last_refill)
del self.buckets[oldest[0]]
self.buckets[client_id] = TokenBucket(
capacity=self.burst,
refill_rate=self.rps
)
bucket = self.buckets[client_id]
allowed = bucket.consume()
return allowed, {
'X-RateLimit-Limit': str(self.burst),
'X-RateLimit-Remaining': str(int(bucket.available_tokens())),
'X-RateLimit-Reset': str(int(time.time()) + 3600)
}
Async version for asyncio-based applications
class AsyncRateLimiter:
"""Async rate limiter for high-performance applications"""
def __init__(
self,
requests_per_second: float = 10,
burst_size: int = 20,
max_clients: int = 100000
):
self.buckets: Dict[str, TokenBucket] = {}
self.rps = requests_per_second
self.burst = burst_size
self.max_clients = max_clients
self.lock = asyncio.Lock()
async def check_rate_limit(self, client_id: str) -> tuple[bool, dict]:
async with self.lock:
if client_id not in self.buckets:
if len(self.buckets) >= self.max_clients:
oldest = min(self.buckets.items(), key=lambda x: x[1].last_refill)
del self.buckets[oldest[0]]
self.buckets[client_id] = TokenBucket(
capacity=self.burst,
refill_rate=self.rps
)
bucket = self.buckets[client_id]
allowed = bucket.consume()
return allowed, {
'X-RateLimit-Limit': str(self.burst),
'X-RateLimit-Remaining': str(int(bucket.available_tokens())),
'Retry-After': '1' if not allowed else '0'
}
2. Sliding Window Counter
Algorithm นี้ให้ความแม่นยำมากกว่า fixed window โดยไม่มี boundary spike ปัญหาคือใช้ memory มากกว่าเพราะต้องเก็บ timestamp ของทุก request
"""
Sliding Window Rate Limiter
More accurate than fixed window, no boundary spike
"""
from collections import deque
from time import monotonic
import threading
from typing import Dict, Tuple
class SlidingWindowRateLimiter:
"""
Sliding window rate limiter using Redis-like sorted sets approach
Implemented in-memory for single-node deployment
"""
def __init__(
self,
max_requests: int = 100,
window_seconds: int = 60,
max_clients: int = 50000
):
self.max_requests = max_requests
self.window_ms = window_seconds * 1000
self.max_clients = max_clients
self.requests: Dict[str, deque] = {}
self.lock = threading.RLock()
self._cleanup()
def _cleanup(self) -> None:
"""Remove old entries periodically"""
now = monotonic() * 1000
cutoff = now - self.window_ms
with self.lock:
for client_id in list(self.requests.keys()):
# Remove expired timestamps
while self.requests[client_id] and self.requests[client_id][0] < cutoff:
self.requests[client_id].popleft()
# Remove empty queues
if not self.requests[client_id]:
del self.requests[client_id]
def is_allowed(self, client_id: str) -> Tuple[bool, Dict[str, str]]:
"""
Check if request is allowed under rate limit
Returns (allowed, headers)
"""
now = monotonic() * 1000
cutoff = now - self.window_ms
with self.lock:
# Initialize or get existing window
if client_id not in self.requests:
if len(self.requests) >= self.max_clients:
# Remove client with fewest requests
min_client = min(
self.requests.items(),
key=lambda x: len(x[1])
)
del self.requests[min_client[0]]
self.requests[client_id] = deque()
window = self.requests[client_id]
# Remove expired entries
while window and window[0] < cutoff:
window.popleft()
# Check if under limit
if len(window) < self.max_requests:
window.append(now)
remaining = self.max_requests - len(window)
return True, {
'X-RateLimit-Limit': str(self.max_requests),
'X-RateLimit-Remaining': str(remaining),
'X-RateLimit-Window': str(self.window_ms),
'X-RateLimit-Reset': str(int((now + self.window_ms) / 1000))
}
# Rate limited
oldest = window[0]
retry_after_ms = oldest + self.window_ms - now
return False, {
'X-RateLimit-Limit': str(self.max_requests),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Window': str(self.window_ms),
'Retry-After': str(int(retry_after_ms / 1000) + 1)
}
Production-ready middleware for FastAPI
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
class RateLimitMiddleware(BaseHTTPMiddleware):
"""FastAPI middleware for rate limiting"""
def __init__(
self,
app,
limiter: SlidingWindowRateLimiter,
key_func=lambda req: req.client.host
):
super().__init__(app)
self.limiter = limiter
self.key_func = key_func
async def dispatch(self, request: Request, call_next):
client_id = self.key_func(request)
allowed, headers = self.limiter.is_allowed(client_id)
if not allowed:
return JSONResponse(
status_code=429,
content={
'error': 'Too Many Requests',
'message': 'Rate limit exceeded. Please try again later.',
'retry_after': headers.get('Retry-After', '60')
},
headers=headers
)
response = await call_next(request)
# Add rate limit headers to successful responses
for key, value in headers.items():
response.headers[key] = value
return response
Example usage with FastAPI
app = FastAPI()
limiter = SlidingWindowRateLimiter(
max_requests=100, # 100 requests
window_seconds=60, # per minute
max_clients=10000
)
app.add_middleware(RateLimitMiddleware, limiter=limiter)
การออกแบบ Distributed Rate Limiter
สำหรับระบบที่มีหลาย server instance การใช้ in-memory rate limiter จะไม่เพียงพอ เพราะแต่ละ instance จะมี counter แยกกัน ทำให้ผู้ใช้สามารถ bypass rate limit ได้โดยการกระจาย request ไปยังหลาย server
"""
Distributed Rate Limiter using Redis
Supports multiple rate limit strategies
"""
import redis
import time
import json
from typing import Tuple, Dict, Optional
from enum import Enum
class RateLimitStrategy(Enum):
FIXED_WINDOW = "fixed"
SLIDING_WINDOW = "sliding"
TOKEN_BUCKET = "token_bucket"
LEAK_BUCKET = "leak_bucket"
class RedisRateLimiter:
"""
Production distributed rate limiter using Redis
Supports multiple algorithms and Lua scripting for atomicity
"""
# Lua script for fixed window - atomic increment and check
FIXED_WINDOW_SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = math.floor(now / window) * window
local current = redis.call('GET', key)
if current == false then
current = 0
else
current = tonumber(current)
end
if current >= limit then
return {0, limit, current, window_start + window - now}
end
current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
return {1, limit, current, 0}
"""
# Lua script for sliding window log
SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cutoff = now - window
-- Remove old entries
redis.call('ZREMRANGEBYSCORE', key, '-inf', cutoff)
-- Count current requests
local current = redis.call('ZCARD', key)
if current >= limit then
-- Get oldest entry to calculate retry time
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
local retry_after = 0
if oldest and #oldest >= 2 then
retry_after = tonumber(oldest[2]) + window - now
end
return {0, limit, current, retry_after}
end
-- Add new request
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, window)
return {1, limit, current + 1, 0}
"""
# Lua script for token bucket
TOKEN_BUCKET_SCRIPT = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local tokens_requested = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_update')
local tokens = tonumber(bucket[1])
local last_update = tonumber(bucket[2])
if tokens == nil then
tokens = capacity
last_update = now
end
-- Calculate token refill
local elapsed = now - last_update
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens >= tokens_requested then
tokens = tokens - tokens_requested
redis.call('HMSET', key, 'tokens', tokens, 'last_update', now)
redis.call('EXPIRE', key, 3600)
return {1, capacity, tokens, 0}
end
-- Not enough tokens
redis.call('HMSET', key, 'tokens', tokens, 'last_update', now)
redis.call('EXPIRE', key, 3600)
local retry_after = math.ceil((tokens_requested - tokens) / refill_rate)
return {0, capacity, tokens, retry_after}
"""
def __init__(
self,
redis_url: str = "redis://localhost:6379/0",
strategy: RateLimitStrategy = RateLimitStrategy.SLIDING_WINDOW,
default_limit: int = 100,
default_window: int = 60,
key_prefix: str = "ratelimit:"
):
self.redis = redis.from_url(redis_url)
self.strategy = strategy
self.default_limit = default_limit
self.default_window = default_window
self.key_prefix = key_prefix
# Register Lua scripts
self._fixed_script = self.redis.register_script(self.FIXED_WINDOW_SCRIPT)
self._sliding_script = self.redis.register_script(self.SLIDING_WINDOW_SCRIPT)
self._token_bucket_script = self.redis.register_script(self.TOKEN_BUCKET_SCRIPT)
def _get_key(self, identifier: str, endpoint: str = "default") -> str:
"""Generate Redis key for rate limiting"""
return f"{self.key_prefix}{endpoint}:{identifier}"
def check_rate_limit(
self,
client_id: str,
limit: Optional[int] = None,
window: Optional[int] = None,
endpoint: str = "default"
) -> Tuple[bool, Dict[str, str]]:
"""
Check rate limit for client
Returns (allowed, headers_dict)
"""
limit = limit or self.default_limit
window = window or self.default_window
key = self._get_key(client_id, endpoint)
now = time.time()
if self.strategy == RateLimitStrategy.FIXED_WINDOW:
result = self._fixed_script(
keys=[key],
args=[limit, window, now]
)
elif self.strategy == RateLimitStrategy.SLIDING_WINDOW:
result = self._sliding_script(
keys=[key],
args=[limit, window * 1000, now * 1000]
)
elif self.strategy == RateLimitStrategy.TOKEN_BUCKET:
# Token bucket needs refill_rate in tokens per second
refill_rate = limit / window
result = self._token_bucket_script(
keys=[key],
args=[limit, refill_rate, 1, now]
)
else:
raise ValueError(f"Unknown strategy: {self.strategy}")
allowed = bool(result[0])
retry_after = int(result[3]) if result[3] > 0 else 0
headers = {
'X-RateLimit-Limit': str(int(result[1])),
'X-RateLimit-Remaining': str(max(0, int(result[1]) - int(result[2]))),
'X-RateLimit-Window': str(window),
}
if not allowed:
headers['Retry-After'] = str(retry_after)
return allowed, headers
def get_remaining(
self,
client_id: str,
endpoint: str = "default"
) -> int:
"""Get remaining requests for client"""
key = self._get_key(client_id, endpoint)
if self.strategy == RateLimitStrategy.FIXED_WINDOW:
current = self.redis.get(key)
if current is None:
return self.default_limit
return max(0, self.default_limit - int(current))
elif self.strategy == RateLimitStrategy.SLIDING_WINDOW:
window = self.default_window * 1000
cutoff = (time.time() * 1000) - window
self.redis.zremrangebyscore(key, '-inf', cutoff)
current = self.redis.zcard(key)
return max(0, self.default_limit - current)
return 0
def reset(self, client_id: str, endpoint: str = "default") -> bool:
"""Reset rate limit for client"""
key = self._get_key(client_id, endpoint)
return bool(self.redis.delete(key))
Example: Tiered rate limiting for different plans
class TieredRateLimiter:
"""Rate limiter with different limits per subscription tier"""
TIERS = {
'free': {'requests': 60, 'window': 60}, # 60 req/min
'basic': {'requests': 600, 'window': 60}, # 600 req/min
'pro': {'requests': 6000, 'window': 60}, # 6000 req/min
'enterprise': {'requests': 60000, 'window': 60}, # 60000 req/min
}
def __init__(self, redis_url: str):
self.redis_limiter = RedisRateLimiter(
redis_url=redis_url,
strategy=RateLimitStrategy.SLIDING_WINDOW
)
def check(self, user_id: str, tier: str, endpoint: str = "api") -> Tuple[bool, Dict]:
"""Check rate limit based on user tier"""
tier_config = self.TIERS.get(tier, self.TIERS['free'])
return self.redis_limiter.check_rate_limit(
client_id=user_id,
limit=tier_config['requests'],
window=tier_config['window'],
endpoint=endpoint
)
การ Integrate กับ AI Provider
เมื่อเข้าใจ algorithm แล้ว มาดูว่าจะนำไปใช้กับ AI API request อย่างไร โดยเฉพาะการ integrate กับ HolySheep AI ที่ให้บริการ API ราคาประหยัดมาก
"""
AI API Rate Limiter with Cost Control
Integrates with HolySheep AI API for production use
"""
import asyncio
import aiohttp
import time
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass
from enum import Enum
import json
class Model(Enum):
"""Supported AI models with pricing (per 1M tokens)"""
GPT4_1 = {"id": "gpt-4.1", "input": 8.0, "output": 8.0}
CLAUDE_SONNET_45 = {"id": "claude-sonnet-4.5", "input": 15.0, "output": 15.0}
GEMINI_25_FLASH = {"id": "gemini-2.5-flash", "input": 2.50, "output": 2.50}
DEEPSEEK_V32 = {"id": "deepseek-v3.2", "input": 0.42, "output": 0.42}
@dataclass
class CostBudget:
"""Cost control configuration"""
daily_limit: float = 100.0 # Max $100 per day
monthly_limit: float = 2000.0 # Max $2000 per month
per_request_max: float = 0.50 # Max $0.50 per request
class AIRequestLimiter:
"""
Intelligent rate limiter for AI API calls
- Per-user rate limiting
- Cost budgeting and tracking
- Automatic fallback between providers
- Retry with exponential backoff
"""
BASE_URL = "https://api.holysheep.ai/v1" # HolySheep AI endpoint
def __init__(
self,
api_key: str,
redis_url: str = "redis://localhost:6379/0",
cost_budget: Optional[CostBudget] = None
):
from redis_rate_limiter import RedisRateLimiter, RateLimitStrategy
self.api_key = api_key
self.cost_budget = cost_budget or CostBudget()
# Redis-backed rate limiter
self.rate_limiter = RedisRateLimiter(
redis_url=redis_url,
strategy=RateLimitStrategy.SLIDING_WINDOW,
default_limit=1000,
default_window=60
)
# Cost tracking Redis keys
self.redis = self.rate_limiter.redis
self._cost_prefix = "ai_cost:"
# Semaphore for concurrent request control
self.semaphore = asyncio.Semaphore(100)
def _estimate_cost(
self,
model: str,
input_tokens: int,
output_tokens: int
) -> float:
"""Estimate request cost based on token usage"""
model_info = None
for m in Model:
if m.value["id"] == model:
model_info = m.value
break
if not model_info:
return 0.0
input_cost = (input_tokens / 1_000_000) * model_info["input"]
output_cost = (output_tokens / 1_000_000) * model_info["output"]
return input_cost + output_cost
def _get_cost_period(self) -> Tuple[str, int]:
"""Get current cost period (daily/monthly)"""
now = time.time()
day_start = now - (now % 86400)
month_start = now - (now % (86400 * 30))
return day_start, month_start
def _update_cost_tracking(
self,
user_id: str,
cost: float
) -> bool:
"""
Update cost tracking for user
Returns False if budget exceeded
"""
day_start, month_start = self._get_cost_period()
# Check daily budget
daily_key = f"{self._cost_prefix}daily:{user_id}:{int(day_start)}"
monthly_key = f"{self._cost_prefix}monthly:{user_id}:{int(month_start)}"
# Get current costs
current_daily = float(self.redis.get(daily_key) or 0)
current_monthly = float(self.redis.get(monthly_key) or 0)
# Check budgets
if current_daily + cost > self.cost_budget.daily_limit:
return False
if current_monthly + cost > self.cost_budget.monthly_limit:
return False
if cost > self.cost_budget.per_request_max:
return False
# Update costs atomically
pipe = self.redis.pipeline()
pipe.incrbyfloat(daily_key, cost)
pipe.expire(daily_key, 86400 * 2)
pipe.incrbyfloat(monthly_key, cost)
pipe.expire(monthly_key, 86400 * 60)
pipe.execute()
return True
async def chat_completion(
self,
user_id: str,
messages: List[Dict],
model: str = "deepseek-v3.2",
max_tokens: int = 2048,
temperature: float = 0.7
) -> Tuple[Optional[Dict], Dict]:
"""
Make rate-limited AI API request
Returns:
(response_data, metadata) or (None, error_metadata)
"""
# Estimate cost before making request
estimated_input = sum(
len(str(m.get('content', ''))) // 4
for m in messages
)
estimated_output = max_tokens
estimated_cost = self._estimate_cost(
model, estimated_input, estimated_output
)
# Check cost budget
if not self._update_cost_tracking(user_id, estimated_cost):
return None, {
'error': 'cost_exceeded',
'message': 'Monthly or daily cost budget exceeded',
'retry_after': 86400
}
# Check rate limit
allowed, headers = self.rate_limiter.check_rate_limit(
client_id=user_id,
limit=1000,