Rate Limiting: چرا هر API باید یک سقف مصرف داشته باشه
فرض کن یک endpoint ساده داری که لیست محصولات رو برمیگردونه. یک روز متوجه میشی سرورت زیر فشار عجیبی رفته، درحالیکه تعداد کاربر واقعی زیاد نشده. وقتی لاگها رو نگاه میکنی، میبینی یک IP تنها، چند هزار درخواست در ثانیه فرستاده — یا یک اسکریپت خرابه، یا یک نفر داره عمداً دادههات رو میخراشه (scrape میکنه)، یا حتی داره تلاش میکنه با حجم درخواست سرویس رو از کار بندازه. rate limiting دقیقاً همون لایهایه که جلوی این سناریو رو میگیره: تعیین میکنه هر کلاینت (بر اساس IP، کاربر، یا API key) توی یک بازهی زمانی مشخص، حداکثر چند درخواست میتونه بفرسته.
چرا واقعاً مهمه
اولین و واضحترین دلیل، محافظت از منابع سرور در برابر سوءاستفاده یا حملات است. حملات denial-of-service (چه عمدی چه ناشی از یک باگ توی کلاینت که وارد یک حلقهی بینهایت از درخواست شده) میتونن یک سرویس سالم رو در عرض چند ثانیه از پا دربیارن. rate limiting یک سقف مشخص میذاره که جلوی این نوع فشار رو میگیره، قبل از اینکه به لایههای عمیقتر سیستم (مثل دیتابیس) برسه.
دومی، توزیع عادلانهی منابع بین کاربرهاست. اگه یک API عمومی داری که چند هزار کلاینت مختلف ازش استفاده میکنن، بدون rate limiting هیچ تضمینی نیست که یک کلاینت پرمصرف، کل ظرفیت سرور رو برای بقیه اشغال نکنه. با یک سقف مشخص برای هر کلاینت، مطمئن میشی هیچکس نمیتونه سهم بقیه رو بخوره.
سومی، کنترل هزینه است، مخصوصاً وقتی endpoint پشت سرش یک سرویس گرونقیمت باشه — مثلاً یک API که هر درخواستش یک فراخوانی به یک مدل هوش مصنوعی یا یک سرویس ثالث پولی میفرسته. بدون سقف، یک باگ ساده توی کد کلاینت (مثلاً یک حلقهی retry بیپایان) میتونه صورتحساب رو در عرض چند ساعت نجومی کنه.
چهارمی، جلوگیری از brute-force. روی endpoint هایی مثل لاگین، rate limiting جلوی این رو میگیره که یک مهاجم بتونه با امتحانکردن هزاران رمز عبور در ثانیه، حساب کاربری رو بشکنه.
الگوریتمهای رایج rate limiting
چند روش استاندارد برای پیادهسازی rate limiting وجود داره، و هرکدوم trade-off متفاوتی دارن.
Fixed Window
سادهترین روش: یک بازهی زمانی ثابت (مثلاً هر دقیقه) در نظر میگیری و میشماری چند درخواست توش اومده. اگه از سقف رد بشه، بقیه رد میشن تا شروع بازهی بعدی.
// pseudo-code ساده برای fixed window
const requests = {}; // { userId: { count, windowStart } }
function isAllowed(userId, limit = 100, windowMs = 60000) {
const now = Date.now();
const entry = requests[userId];
if (!entry || now - entry.windowStart > windowMs) {
requests[userId] = { count: 1, windowStart: now };
return true;
}
if (entry.count >= limit) return false;
entry.count++;
return true;
}
مشکل این روش یک نکتهی ظریفه: چون بازهها ثابت و جدا از همن، یک کلاینت میتونه دقیقاً لحظهی آخر یک بازه ۱۰۰ درخواست بفرسته و دقیقاً لحظهی اول بازهی بعدی، ۱۰۰ درخواست دیگه — یعنی توی یک بازهی دو ثانیهای واقعی، ۲۰۰ درخواست رد شده، درحالیکه سقف روی کاغذ ۱۰۰ در دقیقه بود. به این پدیده burst at boundary میگن.
Sliding Window
این روش با نگهداشتن timestamp دقیق هر درخواست (یا یک تقریب وزندار از دو بازهی متوالی)، مشکل burst مرزی رو حل میکنه. دقیقتره ولی حافظه و محاسبات بیشتری میخواد، چون باید یا timestamp های تکتک درخواستها رو نگه داری، یا یک فرمول ترکیبی حساب کنی.
Token Bucket
این یکی از محبوبترین الگوریتمهاست، چون هم دقیقه، هم به کلاینت اجازهی burst کنترلشده میده. تصورش اینطوریه: یک سطل با ظرفیت مشخص (مثلاً ۱۰ توکن) داری که با نرخ ثابتی (مثلاً یک توکن در ثانیه) پر میشه. هر درخواست یک توکن مصرف میکنه؛ اگه سطل خالی باشه، درخواست رد میشه. مزیتش اینه که اگه کلاینت مدتی درخواستی نفرستاده باشه، سطلش پر مونده و میتونه یک burst کوتاه (تا سقف ظرفیت سطل) بفرسته، ولی نمیتونه برای مدت طولانی بالاتر از نرخ میانگین ادامه بده.
class TokenBucket {
constructor(capacity, refillRatePerSec) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRatePerSec;
this.lastRefill = Date.now();
}
tryConsume() {
this._refill();
if (this.tokens < 1) return false;
this.tokens -= 1;
return true;
}
_refill() {
const now = Date.now();
const elapsedSec = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillRate);
this.lastRefill = now;
}
}
Leaky Bucket
مشابه token bucket ولی از زاویهی دیگه: درخواستها توی یک صف (سطل) قرار میگیرن و با نرخ ثابتی از صف خارج و پردازش میشن، مثل یک سطل که از یک سوراخ ثابت آب نشت میکنه. اگه صف پر بشه، درخواستهای جدید رد میشن. این روش خروجی رو کاملاً یکنواخت نگه میداره (بدون burst)، برخلاف token bucket که burst محدود رو مجاز میدونه — انتخاب بین این دو به این بستگی داره که آیا میخوای نرخ خروجی کاملاً ثابت باشه، یا کمی انعطاف برای burst داشته باشه.
کجا پیادهسازیش کنیم
rate limiting رو میشه در چند لایه پیاده کرد. توی سطح Nginx یا یک reverse proxy، با ماژول limit_req میشه بدون تغییر کد اپلیکیشن، محدودیت گذاشت:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
توی سطح اپلیکیشن، معمولاً از Redis برای نگهداشتن شمارندهها استفاده میشه، چون Redis سریع و atomic عمل میکنه و اگه چند instance از سرور اپلیکیشن داشته باشی، همه بتونن یک شمارندهی مشترک رو ببینن (که با یک شمارندهی in-memory جدا روی هر instance، امکانش نیست).
توی سطح API Gateway (مثل Kong، AWS API Gateway، یا Cloudflare)، rate limiting معمولاً بهصورت آماده و قابلتنظیم از طریق پنل مدیریت وجود داره، بدون نیاز به نوشتن کد.
پاسخ درست به کلاینت رد شده
وقتی یک درخواست بهخاطر rate limit رد میشه، باید کد وضعیت HTTP استاندارد 429 Too Many Requests رو برگردونی، و بهتره هدر Retry-After رو هم اضافه کنی تا کلاینت بدونه دقیقاً چند ثانیه دیگه باید صبر کنه قبل از تلاش مجدد. این جزئیات کوچیک، تفاوت بین یک API قابلاعتماد و یکی که کلاینتهاش رو گیج میکنه رو میسازه.
چطوری بهتره استفاده بشه ؟
Rate limiting را میتوان در چند لایه مختلف پیادهسازی کرد؛ از لایه زیرساخت مثل Nginx و API Gateway گرفته تا داخل خود اپلیکیشن. انتخاب محل مناسب بستگی دارد به اینکه هدف اصلی، محافظت از منابع سیستم است یا کنترل رفتار کاربران و قوانین کسبوکار.
Rate Limiting در Nginx / Reverse Proxy
Nginx در لایه ورودی سیستم قرار میگیرد و قبل از رسیدن درخواست به اپلیکیشن تصمیم میگیرد که درخواست اجازه عبور دارد یا نه. مزیت اصلی این روش این است که درخواستهای اضافی اصلاً وارد برنامه نمیشوند و منابعی مثل CPU، حافظه و اتصالهای دیتابیس مصرف نمیشوند.
این روش برای جلوگیری از فشار زیاد، حملات حجمی، crawlerها و درخواستهای غیرعادی بسیار مناسب است.
مزایا:
-
عملکرد بسیار بالا
-
کاهش فشار روی اپلیکیشن
-
عدم نیاز به تغییر کد برنامه
محدودیت اصلی آن این است که معمولاً فقط به اطلاعاتی مثل IP و Header دسترسی دارد و نمیتواند تفاوت بین کاربران، پلنها یا سطح دسترسی آنها را تشخیص دهد.
مثلاً نمیتواند بهتنهایی تصمیم بگیرد که یک کاربر رایگان فقط ۱۰۰ درخواست در دقیقه داشته باشد و یک کاربر حرفهای ۱۰۰۰۰ درخواست.
Rate Limiting در Middleware اپلیکیشن
در این روش محدودیت داخل خود برنامه اعمال میشود. چون اپلیکیشن اطلاعات بیشتری درباره کاربر دارد، میتواند قوانین دقیقتری بر اساس User ID، API Key، نقش کاربر یا نوع اشتراک اعمال کند.
برای مثال، یک سرویس هوش مصنوعی میتواند برای کاربران رایگان محدودیت ۱۰ درخواست در روز و برای کاربران پولی محدودیت ۱۰۰۰ درخواست در روز قرار دهد.
مزایا:
-
دسترسی به اطلاعات کامل کاربر
-
مناسب برای قوانین کسبوکار
-
انعطافپذیری بالا
اما مشکل این روش این است که درخواست ابتدا وارد اپلیکیشن میشود و سپس محدود میشود. همچنین در معماریهای چند سروره، نگهداری شمارنده در حافظه داخلی هر سرور باعث ایجاد خطا میشود.
برای مثال اگر یک کاربر ۱۰۰ درخواست ارسال کند و Load Balancer آن را بین دو سرور تقسیم کند، هر سرور ممکن است فقط ۵۰ درخواست ببیند و محدودیت بهدرستی اعمال نشود.
به همین دلیل معمولاً از Redis یا یک ذخیرهساز مشترک برای نگهداری وضعیت rate limit استفاده میشود.
Rate Limiting در API Gateway
API Gateway یک لایه مدیریتی بین کلاینت و سرویسهای مختلف قرار میگیرد و معمولاً برای معماریهای بزرگ و چندسرویسی استفاده میشود.
این لایه میتواند محدودیتهای مشترکی برای تمام APIها اعمال کند، API Key مدیریت کند، quota تعریف کند و سیاستهای امنیتی را بهصورت متمرکز اجرا کند.
مزایا:
-
مناسب برای Microserviceها
-
مدیریت متمرکز APIها
-
پشتیبانی از API Key و quota
در مقابل، پیچیدگی و هزینه بیشتری نسبت به Nginx یا middleware ساده دارد و برای پروژههای کوچک ممکن است بیش از نیاز باشد.
بهترین رویکرد: ترکیبی از چند لایه
در سیستمهای واقعی معمولاً از یک روش واحد استفاده نمیشود، بلکه چند لایه با هم کار میکنند.
یک معماری رایج:
Client
|
v
Cloudflare / WAF
|
v
Nginx
(IP Rate Limit)
|
v
Application
(User/API Key Limit)
|
v
Redis
(Shared Counter)
|
v
Database
در این معماری:
-
Nginx جلوی فشار غیرعادی روی زیرساخت را میگیرد.
-
Middleware اپلیکیشن محدودیتهای مربوط به کاربران و منطق کسبوکار را اجرا میکند.
-
Redis وضعیت rate limit را بین چند سرور هماهنگ نگه میدارد.
-
API Gateway در سیستمهای بزرگ مدیریت متمرکز APIها را انجام میدهد.
مثال:
برای یک وبسایت بلاگی متوسط، یک نقطه شروع منطقی میتواند این باشد:
- محدودیت عمومی در Nginx:
حدود 50 تا 100 درخواست در ثانیه برای هر IP - محدودیت در سطح کاربر:
حدود 1000 تا 3000 درخواست در ساعت که مثلا از ۱۰۰۰ تا برای مهمان و ۳۰۰۰ برای بالاترین سطح دسترسی قبل از ادمین - endpointهای حساس:
بین 3 تا 10 درخواست در دقیقه
اما این اعداد قطعی نیستند. یک وبسایت خبری با میلیونها بازدید روزانه، یک وبلاگ شخصی و یک SaaS با API عمومی، نیازهای کاملاً متفاوتی دارند.
عدد صحیح زمانی مشخص میشود که رفتار واقعی کاربران، میانگین درخواستها، زمان پاسخ API، ظرفیت سرور و مصرف منابع بررسی شود. Rate limiting باید بر اساس ظرفیت سیستم تنظیم شود، نه صرفاً بر اساس یک عدد از پیش تعیینشده.
جمعبندی
rate limiting یکی از اون تصمیمهای زیرساختیه که تا نیازش رو حس نکنی، شاید حس کنی زیادیه، ولی وقتی یک باگ توی یک کلاینت یا یک حملهی ساده بدون این لایه سرورت رو از پا دربیاره، میفهمی چرا این یکی از اولین چیزهاییه که هر API عمومی باید از روز اول داشته باشه، نه چیزی که بعد از اولین حادثه اضافهاش میکنی.