Rate Limiting at the Gateway
Fixed window, sliding window, token bucket — service code-এ হাত না দিয়েই আপনার backend-কে abuse থেকে বাঁচান আর fair usage enforce করুন।
বাস্তব জীবনের উদাহরণ
সাবওয়ে স্টেশনের একটা turnstile — এটা একবারে একজনকে যেতে দেয়, একটা গতি বজায় রাখে, আর আপনি কে বা কোথায় যাচ্ছেন তা নিয়ে মাথা ঘামায় না। প্ল্যাটফর্ম (আপনার backend) কখনো ভিড় দেখে না; সে শুধু একটা স্থির স্রোত দেখে।
গল্পে বুঝি
শহরের সবচেয়ে বড় অ্যামিউজমেন্ট পার্ক চালান আল-খোয়ারিজমি। ভেতরে রোলার কোস্টার, খাবারের স্টল, বাথরুম — সব আছে, কিন্তু পুরো পার্কে ঢোকার রাস্তা মাত্র একটাই, সামনের মেইন গেট। ছুটির দিনে যদি হাজার হাজার লোক একসাথে হুড়মুড়িয়ে ঢুকে পড়ে, তাহলে প্রতিটা রাইডে বিপজ্জনক ভিড় জমে, খাবারের লাইন গেটের বাইরে চলে যায়, বাথরুম উপচে পড়ে — পুরো পার্ক অচল। তাই আল-খোয়ারিজমি একটা সহজ নিয়ম বসালেন: মেইন গেট দিয়ে ঘণ্টায় সর্বোচ্চ এত হাজার লোককেই ঢুকতে দেওয়া হবে, তার বেশি নয়।
গেটের দারোয়ান ফাতিমা আল-ফিহরি একটা কাউন্টার হাতে দাঁড়িয়ে থাকেন। কোটা পূর্ণ হয়ে গেলে বাড়তি ভিজিটরদের হয় বাইরে অপেক্ষা করতে বলা হয়, নয়তো “এই ঘণ্টায় আর জায়গা নেই, একটু পরে আসুন” বলে ফিরিয়ে দেওয়া হয়। মজার ব্যাপার — ভেতরের কোনো রাইড অপারেটরকে আর আলাদা করে ভিড় সামলাতে হয় না। একটা গেটে ভিড় থামালেই ভেতরের সব রাইড, সব স্টল, সব বাথরুম একসাথে বেঁচে যায়।
এই গল্পটাই হলো gateway-তে rate limiting। একটাই মেইন গেট = আপনার API gateway, যেখান দিয়ে সব traffic ঢোকে। ঘণ্টায় ভিজিটর ক্যাপ করা = প্রতি client-এর request rate limit করা। ভেতরের কোনো রাইড অতিরিক্ত ভিড়ে না ভাঙা = সব backend service একসাথে overload থেকে বাঁচা। কোটার বেশি ভিজিটরকে অপেক্ষা করানো বা ফিরিয়ে দেওয়া = gateway থেকে throttle করা বা 429 (Too Many Requests) ফেরত দেওয়া। বাস্তবে Cloudflare, Kong বা AWS API Gateway ঠিক এভাবেই একটা জায়গায় limit বসিয়ে পেছনের ডজনখানেক service-কে একসাথে abuse আর traffic spike থেকে রক্ষা করে — প্রতিটা service-এ আলাদা করে পাহারা বসানোর দরকার পড়ে না।
কেন Gateway-তে
প্রতিটা সার্ভিসে rate limiting অতিরিক্ত আর অসামঞ্জস্যপূর্ণ। gateway-তে করলে আপনি পান:
- limit globally বদলানোর একটাই config
- request কোনো service resource খরচ করার আগেই enforce হওয়া limit
- একত্রিত ভিউ: সব সার্ভিস মিলিয়ে per-user limit, per-service bucket নয়
Fixed Window
একটা fixed time window-এ (যেমন চলতি মিনিট) request গোনা। সহজ, তবে window-এর প্রান্তে একটা burst সমস্যা আছে।
class FixedWindowLimiter {
constructor(
private redis: RedisClient,
private limit: number,
private windowSeconds: number
) {}
async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number }> {
const windowKey = `ratelimit:fw:${key}:${Math.floor(Date.now() / (this.windowSeconds * 1000))}`;
const count = await this.redis.incr(windowKey);
if (count === 1) {
// First request in window — set expiry
await this.redis.expire(windowKey, this.windowSeconds);
}
const allowed = count <= this.limit;
return { allowed, remaining: Math.max(0, this.limit - count) };
}
} edge burst সমস্যা: মিনিটে 60-request limit থাকলে, একটা client 11:59-এ 60টা আর 12:00-এ 60টা পাঠাতে পারে — 2 সেকেন্ডে 120টা request। sliding window এটা ঠিক করে।
Sliding Window
চলতি ক্যালেন্ডার window-এ নয়, শেষ N সেকেন্ডে request গোনা:
class SlidingWindowLimiter {
constructor(
private redis: RedisClient,
private limit: number,
private windowMs: number
) {}
async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number }> {
const now = Date.now();
const windowStart = now - this.windowMs;
const redisKey = `ratelimit:sw:${key}`;
const [, , count] = (await this.redis
.multi()
.zRemRangeByScore(redisKey, '-inf', windowStart) // remove old entries
.zAdd(redisKey, { score: now, value: `${now}-${Math.random()}` })
.zCard(redisKey)
.expire(redisKey, Math.ceil(this.windowMs / 1000))
.exec()) as [unknown, unknown, number, unknown];
const allowed = count <= this.limit;
return { allowed, remaining: Math.max(0, this.limit - count) };
}
} বেশি নির্ভুল, তবে প্রতিটা request-এর জন্য একটা করে Redis entry রাখে। খুব বেশি traffic-এর key-র জন্য sorted set বড় হয়ে যায় — শুধু শেষ N entry রাখতে ZREMRANGEBYRANK দিয়ে cap করুন।
Token Bucket
সবচেয়ে মসৃণ algorithm। একটা bucket একটা স্থির হারে (refill rate) ভরে। প্রতিটা request একটা token খরচ করে। bucket capacity পর্যন্ত burst অনুমোদিত।
class TokenBucketLimiter {
constructor(
private redis: RedisClient,
private capacity: number, // max tokens (burst size)
private refillRate: number // tokens per second
) {}
async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number }> {
const now = Date.now() / 1000; // seconds
const bucketKey = `ratelimit:tb:${key}`;
// Lua script for atomicity
const script = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- Refill tokens based on elapsed time
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + elapsed * refill_rate)
local allowed = 0
if tokens >= requested then
tokens = tokens - requested
allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) + 1)
return { allowed, math.floor(tokens) }
`;
const [allowed, remaining] = (await this.redis.eval(
script,
1,
bucketKey,
this.capacity,
this.refillRate,
now,
1
)) as [number, number];
return { allowed: allowed === 1, remaining };
}
} Response Header
সবসময় client-কে তাদের rate limit status জানান:
function applyRateLimitHeaders(
res: Response,
limit: number,
remaining: number,
resetSeconds: number
): void {
res.set({
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': String(remaining),
'X-RateLimit-Reset': String(Math.floor(Date.now() / 1000) + resetSeconds),
'Retry-After': remaining === 0 ? String(resetSeconds) : undefined
});
}
// When limited:
res.status(429).json({
error: 'Too Many Requests',
retryAfter: resetSeconds
}); Retry-After header ভালো-আচরণের client-কে আপনাকে আরও জোরে হাতুড়ি না মেরে নিজে থেকেই পিছিয়ে যেতে দেয়।
Limit Key
আপনি কীসের উপর limit বসান তা attack surface ঠিক করে দেয়:
function getLimitKey(req: Request): string {
// Option 1: by authenticated user (most fair)
if (req.headers['x-user-id']) {
return `user:${req.headers['x-user-id']}`;
}
// Option 2: by API key
if (req.headers['x-api-key']) {
return `apikey:${hashApiKey(req.headers['x-api-key'] as string)}`;
}
// Option 3: by IP (for unauthenticated routes)
return `ip:${req.ip}`;
} Layered limit — একসাথে একাধিক limit বসানো:
async function checkRateLimits(req: Request): Promise<void> {
const userId = req.headers['x-user-id'] as string;
await Promise.all([
// Global: 1000 req/min per user
limiter.check(`global:${userId}`, 1000, 60),
// Per-route: 100 req/min on expensive endpoints
limiter.check(`route:${req.path}:${userId}`, 100, 60),
// Burst: max 20 req/sec
limiter.check(`burst:${userId}`, 20, 1)
]);
} Kong Rate Limiting Plugin
production-এ নিজের বানানোর চেয়ে battle-tested plugin ব্যবহার করুন:
# Kong declarative config (deck)
plugins:
- name: rate-limiting
config:
minute: 1000
hour: 10000
policy: redis
redis_host: redis
redis_port: 6379
limit_by: consumer # or ip, credential, header
hide_client_headers: false Kong Redis atomicity, header injection আর 429 response সামলায়। আপনার কাজ হলো per route আর per consumer tier limit configure করা।