এই পর্বে যা শিখবেন
- Rate limiting কেন লাগে, আর quota, concurrency limit ও load shedding থেকে এটি কোথায় আলাদা
- কার হিসাবে গুনবেন — user, API key, IP, tenant, endpoint — আর ভাগ করা IP-র ফাঁদ
- Token bucket, leaky bucket, fixed window, sliding log, sliding window counter আর GCRA — গণিত, memory আর ভুলের ধরন
- অনেক সার্ভার মিলে একটি সীমা: Redis, atomic অপারেশন, race condition, local সীমা আর ঘড়ির সমস্যা
- 429, Retry-After আর RateLimit header; adaptive সীমা; আর ইন্টারভিউতে একটি rate limiter নকশা
১ · Rate limiting কেন — ন্যায্যতা, ক্ষমতা রক্ষা, খরচ আর অপব্যবহার থেকে সুরক্ষা
ঈদের আগে রেলস্টেশনের টিকিট কাউন্টারে একটি নিয়ম প্রায়ই চোখে পড়ে: “একজন সর্বোচ্চ চারটি টিকিট”। কাউন্টার খালি থাকলেও নিয়মটি বদলায় না। এর উদ্দেশ্য ভিড় সামলানো নয়। উদ্দেশ্য হলো, কেউ একা পুরো ট্রেনের টিকিট কিনে ফেলতে না পারেন, আর লাইনের শেষ মানুষটিও কিছু পান।
সফটওয়্যারে এই নিয়মের নাম rate limiting: একজন ক্লায়েন্ট নির্দিষ্ট সময়ে কতগুলো অনুরোধ করতে পারবেন, তার সীমা। সীমা পেরোলে সার্ভার কাজটি না করে দ্রুত জানিয়ে দেয় — সাধারণত HTTP 429 Too Many Requests দিয়ে, সাথে কতক্ষণ পরে আবার চেষ্টা করা যাবে।
চারটি কারণ
ন্যায্যতা
একটি ভুল-লেখা script বা একজন বড় গ্রাহক যেন বাকি সবার ভাগ খেয়ে না ফেলেন। ভাগ করা সেবায় একে বলে noisy neighbor সমস্যা।
প্রতি user বা tenant-এ সীমাক্ষমতা রক্ষা
Database আর পেছনের সার্ভিস কতটা নিতে পারে, তা load test-এ জানা। দরজায় সীমা টানলে একজনের হঠাৎ ঢল পুরো সিস্টেম বসাতে পারে না।
দামি endpoint-এ কড়া সীমাখরচ নিয়ন্ত্রণ
কিছু কাজের প্রতিটিতে টাকা লাগে: OTP-র SMS, মানচিত্রের API, AI model-এর call. সীমা না থাকলে একটি bug-ই মাস শেষে বড় বিল আনতে পারে।
মাসের quota, দিনের সীমাঅপব্যবহার থেকে সুরক্ষা
বারবার ভুল password চেষ্টা, অন্যের তথ্য দ্রুত সংগ্রহের bot, বা বিপুল অনুরোধ — rate limit প্রতিরক্ষার একটি স্তর। পুরো প্রতিরক্ষা নয়; বাকি স্তর পর্ব ৫২-এ।
login, OTP, search-একাছাকাছি চারটি ধারণা — গুলিয়ে ফেলবেন না
| ধারণা | কী গোনে | কখন কাজ করে | উদাহরণ |
|---|---|---|---|
| Rate limit | অল্প সময়ে অনুরোধের হার | সবসময়, সার্ভার অলস থাকলেও | প্রতি user মিনিটে ১০০টি |
| Quota | দীর্ঘ সময়ের মোট ব্যবহার | চুক্তি বা প্যাকেজ অনুযায়ী | ফ্রি প্যাকেজে মাসে ১০,০০০ call |
| Concurrency limit | এই মুহূর্তে ভেতরে কতগুলো চলছে | সবসময়, ধীর অনুরোধ বেশি জায়গা নেয় | প্রতি API key-তে একসাথে ২০টি |
| Load shedding | সার্ভারের নিজের ভার | শুধু ভিড়ে, সবার জন্য | ব্যস্ততা ৯৫% ছাড়ালে 503 (পর্ব ১৮) |
Rate limit উত্তর দেয় “এই ক্লায়েন্ট কি নিজের ভাগ ছাড়িয়েছেন?” — তাই উত্তর 429, দোষ ক্লায়েন্টের দিকে। Load shedding উত্তর দেয় “আমি কি এখন আর নিতে পারি?” — তাই উত্তর 503, সমস্যা সার্ভারের দিকে। দুটিই লাগে: এক লাখ বৈধ ব্যবহারকারী একসাথে এলে প্রত্যেকে নিজের সীমায় থেকেও সার্ভার ডোবাতে পারেন।
নিচের ছবিটি এই পর্বের মানচিত্র; পুরো পর্ব জুড়ে এর প্রতিটি অংশে ফিরব।
অনুরোধের পথে rate limiting-এর স্তর
Clients → edge (coarse IP limits) → API gateways with limiter → shared counter store → services
এই ধারণার জন্ম network-এ, web-এর অনেক আগে। ১৯৮০ ও ১৯৯০-এর দশকে ATM আর packet network-এ “প্রতিটি সংযোগ চুক্তির হারের বেশি পাঠাবে না” নিয়ম মানাতে leaky bucket ব্যবহৃত হতো; Jonathan Turner-এর ১৯৮৬ সালের একটি প্রবন্ধকে সাধারণত এর প্রথম বর্ণনা ধরা হয়। ১৯৯৯ সালের RFC 2697 আর 2698 DiffServ network-এর জন্য token bucket-ভিত্তিক meter নির্দিষ্ট করে। আর HTTP-তে 429 কোডটি আসে অনেক পরে, RFC 6585-এ (২০১২)। আজকের API gateway সেই পুরোনো network-এর অঙ্কই HTTP অনুরোধে খাটায়।
২ · কার হিসাব রাখবেন — user, API key, IP, tenant আর endpoint
Algorithm বাছাইয়ের আগে একটি প্রশ্ন: কার নামে গুনবেন? এই পরিচয়কে বলে rate-limit key. ভুল key বাছলে সবচেয়ে নিখুঁত algorithm-ও ভুল মানুষকে আটকায়।
| Key | ভালো দিক | ফাঁদ |
|---|---|---|
| User ID (login-এর পরে) | মানুষ ধরে ন্যায্য ভাগ; ফোন বা Wi-Fi বদলালেও একই | Login-এর আগের পাতায় নেই; একজন অনেক account খুলতে পারেন |
| API key | পার্টনার বা অ্যাপ ধরে চুক্তির হিসাব, বিল | একটি key অনেক ব্যবহারকারীর অ্যাপে থাকলে সবাই এক ভাগে |
| IP ঠিকানা | Auth-এর আগেই জানা, সস্তা; অচেনা ঢল ঠেকায় | NAT আর CGNAT-এ বহু মানুষ এক IP-তে; একজন বহু IP বদলাতে পারেন |
| Tenant (গ্রাহক-প্রতিষ্ঠান) | একটি কোম্পানির সব user মিলে চুক্তির সীমা; noisy neighbor ঠেকায় | একই tenant-এর ভেতরেও একজন বাকিদের ভাগ খেতে পারেন — তাই সাথে user-সীমাও |
| Endpoint | দামি কাজে কড়া, সস্তা কাজে ঢিলা সীমা | শুধু endpoint ধরলে সবাই মিলে এক ভাগ — সাথে আরেকটি key লাগে |
বাস্তবে key প্রায়ই কয়েকটি মিলিয়ে, আর একাধিক নিয়ম একসাথে চলে। একটি কাল্পনিক অনলাইন দোকানের নিয়ম হতে পারে:
IP: মিনিটে ৬০০ — শুধু বড় অস্বাভাবিক ঢল ঠেকাতে, auth-এর আগেই;user: মিনিটে ১২০, সব endpoint মিলিয়ে;user + POST /login: ১৫ মিনিটে ১০টি ভুল চেষ্টা;phone + POST /otp: ১০ মিনিটে ৩টি SMS — খরচ আর অপব্যবহার, দুটোই;tenant: চুক্তি অনুযায়ী সেকেন্ডে ৫০০, তার ভেতরে প্রতি user আলাদা।
যেকোনো একটি ভাঙলেই 429, আর উত্তরে জানানো ভালো কোনটি (অংশ ৭)।
ভাগ করা IP-র ফাঁদ — NAT আর CGNAT
বাসার Wi-Fi-তে পাঁচটি ফোন বাইরের জগতে একটিই public IP দেখায় — router-এর NAT. মোবাইল নেটওয়ার্কে এটি আরও বড় আকারে ঘটে: অপারেটরের CGNAT হাজারো গ্রাহককে কয়েকটি public IP-তে বসায়। IPv4 ঠিকানা ফুরিয়ে আসায় বহু দেশের মোবাইল নেটওয়ার্কে এটি প্রচলিত; বাংলাদেশের মোবাইল ডেটাতেও এমন হওয়াই স্বাভাবিক ধরে নেওয়া নিরাপদ, যদিও অপারেটর আর সময়ভেদে আলাদা। RFC 6598 (২০১২) এই কাজের জন্য আলাদা একটি ঠিকানা-পরিসরই সংরক্ষণ করেছে — বিস্তারিত Computer Networks · NAT-এ।
একটি ছাত্রাবাসের সবাই একটিই ডাকবাক্স থেকে চিঠি পাঠান, আর ডাকঘরের নিয়ম “এক ঠিকানা থেকে দিনে ৫টির বেশি নয়”। প্রথম পাঁচজনের পরে বাকি তিনশো জন আটকে যান — কেউ কোনো ভুল না করেই। IP-ভিত্তিক কড়া সীমা ঠিক এটাই করে।
তিনটি নিয়ম মনে রাখুন। (১) IP-সীমা ঢিলা রাখুন আর শুধু মোটা দাগের সুরক্ষায় ব্যবহার করুন; ন্যায্য ভাগের সীমা টানুন পরিচয় জানার পরে, user বা API key দিয়ে। (২) IPv6-এ একজন গ্রাহক সাধারণত অন্তত একটি পুরো /64 পরিসর পান, তাই একক ঠিকানা না গুনে prefix ধরে গোনাই প্রচলিত (Computer Networks · IPv6 দেখুন)। (৩) Proxy বা load balancer-এর পেছনে আসল IP আসে X-Forwarded-For header-এ — শুধু নিজের বিশ্বস্ত proxy-র বসানো অংশটি বিশ্বাস করুন, ক্লায়েন্টের লেখা অংশ নয় (পর্ব ১১)।
সব অনুরোধ সমান নয় — খরচ ধরে গোনা
একটি ছবি দেখা আর পুরো বছরের অর্ডারের export একই “১টি অনুরোধ” হলে সীমা সম্পদ রক্ষা করে না। তাই অনেক নকশায় প্রতিটি অনুরোধের একটি cost থাকে: সাধারণ GET ১ token, বড় search ৫, export ৫০। কিছু GraphQL API প্রকাশ্যেই query-র আনুমানিক খরচ ধরে “point”-ভিত্তিক সীমা দেয়, পর্ব ৯-এর query cost analysis-এর মতো। Token bucket-এ এটি সহজ: এক token-এর বদলে costটি কাটুন।
৩ · Token bucket আর leaky bucket — গড় হার আর burst
মানুষের ব্যবহার সমান তালে আসে না: অ্যাপ খুলতেই এক সেকেন্ডে ১৫টি অনুরোধ, তারপর দুই মিনিট শুধু পড়া। “সেকেন্ডে ১০টির বেশি নয়” কড়া নিয়ম এই স্বাভাবিক ঝাঁকুনিও আটকায়। দরকার এমন নিয়ম, যা গড়ে হার মানায়, কিন্তু অল্প burst সয়।
Token bucket — বালতিতে token জমে
প্রতিটি key-এর একটি বালতি। প্রতি সেকেন্ডে r-টি token জমে, কিন্তু বালতিতে সর্বোচ্চ b-টি ধরে; উপচে পড়া token হারিয়ে যায়। প্রতিটি অনুরোধ একটি token নেয়। token না থাকলে অনুরোধ ফেরত।
ব্যাংকের কাউন্টারে প্রতি মিনিটে একটি করে টোকেন ছাপা হয়, আর টোকেন-যন্ত্রের খাপে সর্বোচ্চ ৫টি জমে থাকতে পারে। সকালে খুলে দেখলেন ৫টি তৈরি — প্রথম পাঁচজন একসাথে টোকেন পেলেন — এটিই burst. ষষ্ঠজনকে পরের টোকেন ছাপা পর্যন্ত অপেক্ষা করতে হবে। সারাদিনে গড়ে মিনিটে একজনের বেশি নয়, কিন্তু সকালের ছোট ভিড়টুকু সামলানো গেল।
বাস্তবে কেউ প্রতি সেকেন্ডে token যোগ করে না। প্রতি key-তে দুটি সংখ্যা রাখা হয় — শেষ token-সংখ্যা আর শেষ হিসাবের সময় — আর অনুরোধ এলে একবারেই হিসাব (lazy refill): tokens = min(b, tokens + r × (now − last)). নিচের stepper-এ r = সেকেন্ডে ২, b = ৫। শেষ সারিতে একই সিদ্ধান্ত GCRA দিয়ে (অংশ ৫)।
| সময় t | |
| শেষ হিসাবের পর যোগ (refill) | |
| অনুরোধের আগে token | |
| এলো | |
| ছাড়া পেল | |
| ফেরত (429) | |
| পরে বালতি | |
| GCRA-র TAT (অংশ ৫) |
- শুরু। বালতি পূর্ণ, ৫টি token — অনেকক্ষণ চুপ থাকলে এভাবেই থাকে, ৫-এর বেশি জমে না।
- একসাথে ৭টি অনুরোধ। প্রথম ৫টি একটি করে token নিল, বালতি খালি; বাকি ২টি 429. গড় হার সেকেন্ডে ২, তবু এক মুহূর্তে ৫টি — এটিই burst.
- ০.৫ সেকেন্ড পরে ১টি। হিসাব: ০.৫ × ২ = ১টি token জমেছে। অনুরোধটি সেটি নিল, বালতি আবার ০।
- ২.০ সেকেন্ডে ৪টি। শেষ হিসাব ০.৫ সেকেন্ডে, তাই ১.৫ × ২ = ৩টি token. ৩টি ছাড়া, ১টি ফেরত।
- চার সেকেন্ড চুপ, তারপর ৬.০ সেকেন্ড। হিসাবে ৪ × ২ = ৮টি জমার কথা, কিন্তু বালতি ৫-এই থামে। অলস সময় অসীম সঞ্চয় দেয় না — burst-এর ছাদ b.
- সীমার প্রমাণ। ০ থেকে ২.০ সেকেন্ডে ছাড়া পেল ৫ + ১ + ৩ = ৯টি, ঠিক b + r·T = ৫ + ২ × ২ = ৯। যেকোনো সময়খণ্ডে এর বেশি সম্ভব নয়। GCRA একটিমাত্র সংখ্যা (TAT) রেখে প্রতিটি সিদ্ধান্ত হুবহু একই দেয়।
- যেকোনো সময়খণ্ড [t, t + T] নিন। শুরুতে বালতিতে সর্বোচ্চ b-টি token থাকতে পারে।
- এই T সেকেন্ডে নতুন token জমে সর্বোচ্চ r·T-টি (বালতি ভরা থাকলে আরও কম, কারণ উপচানো token হারায়)।
- প্রতিটি ছাড়া পাওয়া অনুরোধ একটি token খরচ করে, আর token ঋণাত্মক হয় না। তাই ছাড়া পাওয়া ≤ b + r·T.
ফল দুটি: দীর্ঘ সময়ে গড় হার কখনো r ছাড়ায় না (T বড় হলে b নগণ্য), আর যেকোনো মুহূর্তে একসাথে সর্বোচ্চ b. নকশার সিদ্ধান্তও দুটি সংখ্যায়: r ঠিক হয় পেছনের ক্ষমতা আর ন্যায্য ভাগ থেকে, b ঠিক হয় স্বাভাবিক ব্যবহারের ঝাঁকুনি থেকে।
Leaky bucket — দুটি আলাদা জিনিস, একটি নাম
ফুটো বালতিতে পানি যত জোরেই ঢালুন, নিচ দিয়ে বেরোয় একই তালে। Leaky bucket নামে দুটি আলাদা পদ্ধতি চলে, আর বই ও documentation প্রায়ই দুটি মিশিয়ে ফেলে:
Leaky bucket as a queue
অনুরোধ সত্যিই লাইনে দাঁড়ায়; প্রতি 1/r সেকেন্ডে একটি করে পেছনের সার্ভিসে যায়। লাইন b-তে ভরে গেলে নতুনটি ফেরত। ফল: বেরোনো স্রোত পুরো মসৃণ, কিন্তু অপেক্ষা যোগ হয় — একে বলে traffic shaping.
পেছনের ব্যবস্থা burst একদম সয় না যখনLeaky bucket as a meter
অনুরোধ অপেক্ষা করে না; শুধু একটি “পানির স্তর” গোনা হয়, যা স্থির হারে কমে। স্তর উপচালে অনুরোধ সঙ্গে সঙ্গে ফেরত — traffic policing. গণিতে এটি token bucket-এর হুবহু আয়নার ছবি।
HTTP API-র প্রায় সব limiterNginx-এর documentation নিজেই limit_req-এর পদ্ধতিকে “leaky bucket” বলে। burst দিলে বাড়তি অনুরোধ অপেক্ষা করে নির্দিষ্ট তালে যায় — queue; সাথে nodelay দিলে অপেক্ষা ছাড়াই সঙ্গে সঙ্গে যায় — তখন আচরণ meter বা token bucket-এর মতো (খুঁটিনাটি সংস্করণভেদে আলাদা; পর্ব ১১-এর config দেখুন)।
এবার দুটি পাশাপাশি চালান: একই seeded অনুরোধ-স্রোত — পেছনে সেকেন্ডে গড়ে ৬টি, আর ২ ও ৬ নম্বর সেকেন্ডে ২০০ ms-এর মধ্যে ৩৫টি করে ঢল। সীমা r = ১০, b = ২০ (leaky queue-র আকারও ২০)।
// Token bucket (meter) vs leaky bucket (queue): same seeded bursty stream, 10 s.
// Limit: r = 10 req/s, b = 20 (token bucket burst = leaky-bucket queue size).
function rng(seed) { // mulberry32: seeded, repeatable
return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const rand = rng(7), R = 10, B = 20, arr = [];
for (let t = 0; (t += -Math.log(1 - rand()) / 6) < 10;) arr.push(t); // ~6 req/s background
for (const s of [2, 6]) for (let i = 0; i < 35; i++) arr.push(s + rand() * 0.2); // 2 bursts of 35
arr.sort((a, b) => a - b);
const row = () => ({ in: 0, tbOk: 0, tbNo: 0, lbOk: 0, lbNo: 0, lbOut: 0 });
const sec = Array.from({ length: 10 }, row);
let tokens = B, last = 0; // token bucket state: 2 numbers
let nextFree = 0, maxWait = 0; const outs = []; // leaky bucket: 1 leaves every 1/R s
for (const t of arr) {
const s = sec[Math.floor(t)]; s.in++;
tokens = Math.min(B, tokens + (t - last) * R); last = t; // lazy refill
if (tokens >= 1) { tokens -= 1; s.tbOk++; } else s.tbNo++;
const queued = outs.filter(o => o > t).length; // still waiting in the bucket
if (queued >= B) { s.lbNo++; continue; }
const out = Math.max(t, nextFree); nextFree = out + 1 / R; outs.push(out);
s.lbOk++; maxWait = Math.max(maxWait, out - t);
if (out < 10) sec[Math.floor(out)].lbOut++;
}
console.log("sec in | token bucket ok rej | leaky queue ok rej sent");
sec.forEach((s, i) => console.log(String(i).padStart(3) + String(s.in).padStart(4) + " |" +
String(s.tbOk).padStart(16) + String(s.tbNo).padStart(4) + " |" + String(s.lbOk).padStart(15) +
String(s.lbNo).padStart(4) + String(s.lbOut).padStart(5)));
const peak = k => Math.max(...sec.map(s => s[k]));
console.log("max admitted in one second: token bucket " + peak("tbOk") + ", leaky bucket sends " + peak("lbOut"));
console.log("leaky bucket longest wait in queue: " + (maxWait * 1000).toFixed(0) + " ms");আউটপুট:
sec in | token bucket ok rej | leaky queue ok rej sent
0 5 | 5 0 | 5 0 5
1 9 | 9 0 | 9 0 9
2 41 | 23 18 | 24 17 10
3 9 | 9 0 | 9 0 10
4 4 | 4 0 | 4 0 10
5 3 | 3 0 | 3 0 10
6 46 | 28 18 | 28 18 10
7 4 | 4 0 | 4 0 10
8 9 | 9 0 | 9 0 10
9 4 | 4 0 | 4 0 10
max admitted in one second: token bucket 28, leaky bucket sends 10
leaky bucket longest wait in queue: 1998 ms
Token bucket: ২ নম্বর সেকেন্ডে ৪১টির মধ্যে ২৩টি ছাড়া, ৬ নম্বরে ৪৬টির মধ্যে ২৮টি — এক সেকেন্ডে সর্বোচ্চ ২৮, যা প্রমাণের ছাদ b + r × 1 = ৩০-এর নিচে। শান্ত সেকেন্ডে কেউ আটকায়নি। পেছনের সার্ভিস তাই এক সেকেন্ডে ২৮টি পর্যন্ত একসাথে পায়।
Leaky queue: প্রায় একই সংখ্যক গৃহীত (২৪ আর ২৮), কিন্তু “sent” কলামে ২ নম্বর সেকেন্ড থেকে প্রতি সেকেন্ডে ঠিক ১০টি — পেছনে মসৃণ স্রোত। দাম: লাইনের শেষজন প্রায় ১,৯৯৮ ms অপেক্ষা করেছেন, অর্থাৎ b ÷ r = ২০ ÷ ১০ = ২ সেকেন্ড। Interactive API-তে এটি প্রায়ই অগ্রহণযোগ্য; তাই HTTP-তে meter চলে, আর queue চলে পেছনের কাজে, যেমন SMS gateway-কে সমান তালে বার্তা দেওয়া।
৪ · Window দিয়ে গোনা — fixed window, sliding log আর sliding window counter
অনেক নিয়ম লেখা হয় window দিয়ে: “মিনিটে ১০০টি”। এর তিনটি প্রচলিত বাস্তবায়ন, প্রতিটির ভুলের ধরন আলাদা।
Fixed window counter — সরল, কিন্তু সীমানায় দ্বিগুণ
Fixed window counter-এ ঘড়ির প্রতিটি পূর্ণ মিনিটের জন্য একটি গণনা: ১০:১৫:০০ থেকে ১০:১৫:৫৯ পর্যন্ত একটি, ১০:১৬:০০-এ নতুন করে শূন্য। প্রতি key-তে একটি সংখ্যা, Redis-এ একটি INCR — খুব সস্তা।
সমস্যা সীমানায়। কেউ ০:৫৯-এ ১০০টি পাঠালেন (প্রথম মিনিটের সীমা পূর্ণ), আর ১:০০-এ আরও ১০০টি (নতুন মিনিট, গণনা শূন্য)। দুই সেকেন্ডের কম সময়ে ২০০টি — সীমার দ্বিগুণ। আর সবার window একই মুহূর্তে শূন্য হলে মিনিটের শুরুতে আটকে থাকারা একসাথে ফেরেন — ছোট একটি thundering herd.
Sliding window log — নিখুঁত, কিন্তু memory-খেকো
Sliding window log প্রতিটি গৃহীত অনুরোধের সময় একটি তালিকায় রাখে। নতুন অনুরোধ এলে ঠিক শেষ ৬০ সেকেন্ডের বাইরের সময়গুলো মুছে বাকিগুলো গোনে; ১০০-র কম হলে ছাড়ে। কোনো সীমানা নেই, তাই কোনো ফাঁকও নেই। দাম: প্রতি ব্যবহারকারীর জন্য সর্বোচ্চ ১০০টি timestamp. Redis-এ সাধারণত একটি sorted set দিয়ে করা হয়, score-এ সময় রেখে (অংশ ৫-এ memory-র হিসাব)।
Sliding window counter — দুটি সংখ্যায় কাছাকাছি অনুমান
মাঝামাঝি পথটি sliding window counter: শুধু এখনকার আর আগের fixed window-এর গণনা রাখুন, আর ধরে নিন আগের window-এর অনুরোধগুলো সমানভাবে ছড়ানো ছিল। এখনকার window-এ f অংশ সময় পেরোলে শেষ ৬০ সেকেন্ডে আগের window-এর (1 − f) অংশ এখনো ভেতরে:
এই পদ্ধতির একটি রূপ Cloudflare-এর ২০১৭ সালের দিকে প্রকাশিত engineering blog-এ বর্ণিত আছে, যেখানে তাঁরা লিখেছেন বাস্তব traffic-এ এর ভুল খুব কম পাওয়া গেছে (প্রকাশিত বর্ণনা অনুযায়ী; আপনার traffic-এ মাপা দরকার)। নিচের stepper-এ সীমা মিনিটে ১০০।
| এখন | |
| আগের window-এর গণনা | |
| এখনকার window-এর গণনা | |
| আগের window-এর ওজন 1 − f | |
| আনুমানিক গণনা | |
| সিদ্ধান্ত |
- দুটি সংখ্যা, একটি ওজন। সময় ১২:০১:১৫, এখনকার window-এর ১৫ সেকেন্ড পেরিয়েছে, f = ১৫/৬০ = ০.২৫। শেষ ৬০ সেকেন্ডে আগের window-এর শেষ ৪৫ সেকেন্ড, অর্থাৎ তার ০.৭৫ অংশ, এখনো ভেতরে। আগের window-এ ছিল ৮৪টি, এখনকারটিতে ৩৬টি।
- নতুন অনুরোধ। অনুমান: ৮৪ × ০.৭৫ + ৩৬ = ৬৩ + ৩৬ = ৯৯, যা ১০০-র কম। ছাড়া পেল, এখনকার গণনা ৩৭।
- একই মুহূর্তে আরেকটি। ৮৪ × ০.৭৫ + ৩৭ = ১০০ — সীমা পূর্ণ, তাই 429. এক সেকেন্ড পরে ওজন ০.৭৩৩, অনুমান ৯৮.৬ — window সরে, তাই জায়গাও ধীরে ধীরে খোলে।
- ১২:০১:৩০। f = ০.৫, তাই আগের ৮৪-র অর্ধেক ধরা হয়: ৪২ + ৩৭ = ৭৯। ছাড়া, গণনা ৩৮।
- ১২:০২:০০ — window বদল। এখনকার ৩৮ “আগের” হলো, নতুন গণনা ০, আর ৮৪-র window পুরো বাদ। অনুমান ৩৮ × ১ + ০ = ৩৮। মাত্র তিনটি সংখ্যা মনে রাখতে হয়: দুটি গণনা আর window-এর পরিচয়।
- অনুমানের দাম। ধাপ ২-এর মুহূর্তে ফিরুন। আগের ৮৪টির সবই যদি ১২:০০:৫০-এর পরে এসে থাকে, আসল শেষ-৬০-সেকেন্ডের গণনা ৮৪ + ৩৬ = ১২০ — sliding log ফেরাত, counter ছেড়েছে। আর সবগুলো ১২:০০:১৫-এর আগে এলে আসল গণনা মাত্র ৩৬ — counter অকারণে কড়া হতে পারত। ভুল দুই দিকেই হয়।
এবার তিনটি পদ্ধতিকে একই traffic দিন। তিনটি পরিস্থিতি: (A) সীমানার দুই পাশে ১০০ + ১০০; (B) মিনিটের শুরুতে ১০০, পরের মিনিটের মাঝে ১০০; (C) মিনিটের শেষে ১০০, পরের মিনিটের মাঝে ১০০। প্রতিটি ঢল ১ সেকেন্ডে (১০ ms অন্তর)।
// Limit: 100 requests per 60 s for one user. Three ways to count the same traffic.
const LIMIT = 100, W = 60;
function fixedWindow() { let win = -1, n = 0;
return t => { const w = Math.floor(t / W); if (w !== win) { win = w; n = 0; }
if (n < LIMIT) { n++; return true; } return false; }; }
function slidingLog() { const log = []; // every accepted timestamp
return t => { while (log.length && log[0] <= t - W) log.shift();
if (log.length < LIMIT) { log.push(t); return true; } return false; }; }
function slidingCounter() { let win = -1, cur = 0, prev = 0; // two counters
return t => { const w = Math.floor(t / W);
if (w !== win) { prev = (w === win + 1) ? cur : 0; cur = 0; win = w; }
const weight = 1 - (t - w * W) / W; // share of previous window still inside
if (prev * weight + cur < LIMIT) { cur++; return true; } return false; }; }
const burst = (start, n) => Array.from({ length: n }, (_, i) => start + i * 0.01);
const cases = {
"A: 100 at 0:59, 100 at 1:00": [burst(59, 100), burst(60, 100)],
"B: 100 at 0:01, 100 at 1:30": [burst(1, 100), burst(90, 100)],
"C: 100 at 0:59, 100 at 1:30": [burst(59, 100), burst(90, 100)] };
console.log("case fixed log counter (allowed: 1st burst + 2nd burst)");
for (const [name, [first, second]] of Object.entries(cases)) {
const cols = [fixedWindow, slidingLog, slidingCounter].map(make => {
const allow = make(), a = first.filter(t => allow(t)).length, b = second.filter(t => allow(t)).length;
return (a + "+" + b).padStart(8); });
console.log(name.padEnd(29) + cols.join(" "));
}
const fx = fixedWindow(), okA = [...burst(59, 100), ...burst(60, 100)].filter(t => fx(t));
console.log("\nfixed window, case A: " + okA.length + " allowed between " + okA[0].toFixed(2) +
" s and " + okA[okA.length - 1].toFixed(2) + " s -> 2x the limit in under 2 s");আউটপুট:
case fixed log counter (allowed: 1st burst + 2nd burst)
A: 100 at 0:59, 100 at 1:00 100+100 100+0 100+2
B: 100 at 0:01, 100 at 1:30 100+100 100+100 100+52
C: 100 at 0:59, 100 at 1:30 100+100 100+0 100+52
fixed window, case A: 200 allowed between 59.00 s and 60.99 s -> 2x the limit in under 2 s
A — সীমানার ফাঁদ: fixed window ৫৯.০০ থেকে ৬০.৯৯ সেকেন্ডে ২০০টি ছেড়েছে — দুই সেকেন্ডের কম সময়ে সীমার দ্বিগুণ। Sliding log নিখুঁত: দ্বিতীয় ঢলে ০, কারণ শেষ ৬০ সেকেন্ডে ইতিমধ্যে ১০০টি। Counter-ও প্রায় ঠিক: মাত্র ২টি বাড়তি (১:০০-এর ঠিক পরে ওজন প্রায় ১, আর সামান্য করে কমে)।
B আর C — অনুমানের দুই দিকের ভুল: দুটিতেই counter দ্বিতীয় ঢলে ৫২টি ছাড়ে, কারণ সে শুধু জানে “আগের মিনিটে ১০০টি”, কখন — জানে না। B-তে আসলে শেষ ৬০ সেকেন্ডে কিছুই ছিল না (log ১০০টি ছাড়ে) — counter বেশি কড়া। C-তে শেষ ৬০ সেকেন্ডে ১০০টি ছিল (log ০ ছাড়ে) — counter ৫২টি বাড়তি ছাড়ে। ছড়ানো traffic-এ ভুল ছোট, সাজানো ঢলে বড়।
৫ · GCRA, তুলনা আর memory-র হিসাব
GCRA — একটি সংখ্যায় পুরো বালতি
GCRA (Generic Cell Rate Algorithm) এসেছে ১৯৯০-এর দশকের ATM network-এর traffic management থেকে। এটি token bucket-এর গণিত একটিমাত্র timestamp দিয়ে করে। ভাবনাটা এমন: নিয়মিত হারে চললে প্রতি T = 1/r সেকেন্ডে একটি অনুরোধ আসার কথা। প্রতি key-তে রাখা হয় শুধু theoretical arrival time (TAT) — পরের অনুরোধ “নিয়ম অনুযায়ী” কখন আসার কথা।
T = 1 / r // emission interval: seconds per request
tau = (b - 1) * T // burst tolerance: how far ahead TAT may run
on request at time now:
tat = max(TAT[key], now)
if tat - now > tau: reject (429); retry after (tat - now - tau)
else: TAT[key] = tat + T; allowঅংশ ৩-এর stepper-এর শেষ সারি এটিই: r = ২, তাই T = ০.৫ সেকেন্ড; b = ৫, তাই tau = ২। প্রথম ৫টি অনুরোধে TAT ০ থেকে ২.৫ হয়; ষষ্ঠটিতে TAT − now = ২.৫ > ২, ফেরত — token bucket-এর সাথে হুবহু মিল, অথচ state একটি সংখ্যা। বাড়তি সুবিধা: tat − now − tau সরাসরি বলে দেয় কত পরে আবার চেষ্টা করা যাবে — Retry-After-এর মান। কিছু open-source limiter (যেমন Redis-এর একটি module) GCRA ব্যবহার করে বলে তাদের documentation জানায় (সংস্করণভেদে যাচাই করুন)।
এক টেবিলে ছয়টি পদ্ধতি
| Algorithm | Burst | প্রতি key-এ state | নিখুঁততা | কখন বেছে নেবেন |
|---|---|---|---|---|
| Token bucket | b পর্যন্ত, নিয়ন্ত্রিত | ২টি সংখ্যা | নিখুঁত (তার নিজের নিয়মে) | বেশিরভাগ API; burst সইতে হয় যখন |
| Leaky bucket (queue) | বেরোয় না, লাইনে জমে | একটি queue | নিখুঁত, কিন্তু অপেক্ষা যোগ হয় | পেছনের ব্যবস্থা মসৃণ হার চায় (SMS, বাইরের API) |
| Leaky bucket (meter) / GCRA | b পর্যন্ত | GCRA-তে ১টি সময় | নিখুঁত, token bucket-এর সমান | খুব বেশি key, ছোট state চাই |
| Fixed window | সীমানায় ২× পর্যন্ত | ১টি গণনা (+ window) | সীমানায় ভুল | দীর্ঘ quota (দিন, মাস), যেখানে ২× ক্ষণিকের ঝুঁকি সহনীয় |
| Sliding log | নেই | সীমার সমান timestamp | নিখুঁত | ছোট সীমা (যেমন ১০ মিনিটে ৩টি OTP), নিরীক্ষা দরকার |
| Sliding window counter | সামান্য, অনুমানের ভুল | ২টি গণনা (+ window) | কাছাকাছি, দুই দিকেই ভুল | বড় সীমা, বিপুল key, window-ভাষার নিয়ম |
Memory — ১ কোটি ব্যবহারকারীর হিসাব
ধরা যাক ১ কোটি (১০M) ব্যবহারকারী, প্রত্যেকের সীমা মিনিটে ১০০। নিচের হিসাবে অনুমানগুলো স্পষ্ট লেখা; আসল store-এ প্রতি key-এর বাড়তি খরচ সংস্করণ আর encoding-ভেদে আলাদা, তাই নিজের store-এ মেপে নিন।
// Memory for limiter state: 10 million users, limit 100 requests per minute each.
// Assumptions (state them!): timestamps 8 bytes, counters 4 bytes, and every user key
// costs a flat 64 bytes of overhead (key string + hash-table entry). Real stores differ.
const USERS = 10e6, LIMIT = 100, KEY = 64;
const algos = [
// [name, bytes of state per user, what is stored]
["sliding log", LIMIT * 8, "up to 100 timestamps x 8 B"],
["sliding log, 64 B/entry", LIMIT * 64, "assume ~64 B per entry in a sorted set"],
["fixed window", 4 + 4, "count + window id"],
["sliding window counter", 4 + 4 + 4, "current + previous count + window id"],
["token bucket", 8 + 8, "tokens (double) + last refill time"],
["GCRA", 8, "one timestamp: theoretical arrival time"],
];
const human = b => b >= 1e9 ? (b / 1e9).toFixed(2) + " GB" : (b / 1e6).toFixed(0) + " MB";
console.log("algorithm state B +key B total for 10M users stored");
for (const [name, state, what] of algos) {
const per = state + KEY;
console.log(name.padEnd(24) + String(state).padStart(8) + String(per).padStart(8) +
human(per * USERS).padStart(21) + " " + what);
}
// Only users active in the last minute need state; the rest expire (TTL).
const active = 0.05 * USERS; // assume 5% active at peak
console.log("\nif only 5% (" + active / 1e6 + "M) are active: sliding log " +
human((LIMIT * 8 + KEY) * active) + ", token bucket " + human((16 + KEY) * active));আউটপুট:
algorithm state B +key B total for 10M users stored
sliding log 800 864 8.64 GB up to 100 timestamps x 8 B
sliding log, 64 B/entry 6400 6464 64.64 GB assume ~64 B per entry in a sorted set
fixed window 8 72 720 MB count + window id
sliding window counter 12 76 760 MB current + previous count + window id
token bucket 16 80 800 MB tokens (double) + last refill time
GCRA 8 72 720 MB one timestamp: theoretical arrival time
if only 5% (0.5M) are active: sliding log 432 MB, token bucket 40 MB
কাঁচা ডেটাতেই sliding log প্রায় ৮.৬৪ GB, বাকিরা ৭২০–৮০০ MB — প্রায় ১১–১২ গুণ পার্থক্য। আসল sorted set-এ প্রতিটি সদস্যের খরচ ৮ byte-এর অনেক বেশি; প্রতি সদস্যে ৬৪ byte ধরলে প্রায় ৬৫ GB — একটি সাধারণ সার্ভারের memory ছাড়িয়ে যায়। Counter-ভিত্তিক পদ্ধতিতে খরচের বড় অংশ আসলে key-এর নিজের overhead (৬৪ byte), state-এর নয়।
শেষ লাইন বাস্তবের কথা মনে করায়: সবাই একসাথে সক্রিয় নন। TTL দিয়ে অলস key মুছে গেলে, ৫% সক্রিয় ধরলে sliding log ৪৩২ MB আর token bucket মাত্র ৪০ MB. তাই প্রশ্নটি “কতজন ব্যবহারকারী” নয়, “এক window-এ কতগুলো সক্রিয় key, আর প্রতিটিতে কত byte” — এটাই আসল প্রশ্ন।
৬ · Distributed rate limiting — ভাগ করা counter, race আর ঘড়ি
এক সার্ভারে limiter মানে memory-তে একটি hash table. কিন্তু gateway বাস্তবে হয়তো বিশটি, আর load balancer একজনের অনুরোধ যেকোনোটিতে পাঠায় (পর্ব ১৪)। প্রতিটি gateway নিজে নিজে “মিনিটে ১০০” গুনলে মোট সীমা কার্যত ২০ × ১০০ = ২,০০০। পর্ব ১১-এ ঠিক এই সতর্কতা ছিল: Nginx-এর গণনা একটি instance-এর ভেতরে। তিনটি পথ আছে।
পথ ১ — কেন্দ্রীয় store: Redis-এ INCR আর EXPIRE
সব gateway একটি ভাগ করা store-এ গোনে, সাধারণত Redis-এ, কারণ এটি memory-তে থাকে, দ্রুত, আর একটি key-এর ওপর command একটির পর একটি চালায় (পর্ব ৩০)। Fixed window-এর সরলতম রূপ Redis-এর নিজের documentation-এও বর্ণিত:
INCR rl:u42:1015 -> (integer) 1 first request in this minute
EXPIRE rl:u42:1015 60 only when INCR returned 1
...
INCR rl:u42:1015 -> (integer) 101 > 100: reply 429INCR নিজে atomic: দুটি gateway একসাথে ডাকলেও একজন ১০০, অন্যজন ১০১ পায় — কেউ একই সংখ্যা পায় না। কিন্তু দুটি command-এর মাঝখানে ফাঁক আছে: INCR-এর পরে gateway crash করলে EXPIRE কখনো চলে না, key চিরকাল থাকে, আর সেই ব্যবহারকারী চিরতরে আটকে যান। প্রতিকার: দুটি একসাথে একটি Lua script-এ, অথবা যেসব সংস্করণে EXPIRE … NX বিকল্প আছে (লেখার সময়ে Redis 7.0 থেকে) সেটি দিয়ে।
Race — “পড়ে, তারপর লেখা” কেন সীমা ফুটো করে
Token bucket-এর মতো জটিল state-এ একটি command যথেষ্ট নয়: পড়তে হয় (token, সময়), হিসাব করতে হয়, তারপর লিখতে হয়। এই তিন ধাপ আলাদা network call হলে মাঝখানে অন্য gateway ঢুকে পড়ে — ক্লাসিক race condition আর lost update — পর্ব ২৪-এ দেখেছেন, আর গোড়ার ধারণা Operating Systems · race condition-এ।
- Gateway A
GET → 99পড়ল - Store
counterভাগ করা - Gateway B
GET → 99পড়ল
| সময় | |
| Gateway A | |
| Gateway B | |
| Store-এ counter | |
| মোট ছাড়া পেল |
- A পড়ল। Counter ৯৯ — একটি জায়গা বাকি। উত্তর আসতে প্রায় ১ ms.
- B-ও পড়ল। A এখনো লেখেনি, তাই B-ও দেখল ৯৯। দুজনেই ভাবছে শেষ জায়গাটি তার।
- A লিখল। ৯৯ < ১০০, তাই অনুরোধ ছাড়া, আর store-এ লিখল ১০০।
- B লিখল — সীমা ফুটো। B-ও ছাড়ল আর লিখল ৯৯ + ১ = ১০০, A-র লেখার ওপর। মোট ১০১টি ছাড়া পেল, অথচ counter বলছে ১০০ — একটি বৃদ্ধি হারিয়ে গেছে — lost update.
- Atomic INCR. এবার কেউ আগে পড়ে না; store নিজেই বাড়ানো আর নতুন মান ফেরত — দুটি এক ধাপে করে। A পেল ১০০, তাই ছাড়া; B পেল ১০১, তাই 429. ঠিক ১০০টি।
- Lua script. পড়া, যাচাই, বাড়ানো আর TTL — সব একটি script-এ, যা চলার সময় অন্য command মাঝে ঢোকে না। জটিল state-এর (token bucket, sliding log) প্রচলিত পথ; ফেরত অনুরোধ counter-ও বাড়ায় না।
সংখ্যায়: দুটি gateway, ২০০ ms-এ ১,০০০টি অনুরোধ, প্রতিটি store call ১–৩ ms; তিন পদ্ধতি একই seeded interleaving-এ।
// Two gateway nodes share one counter (limit 100 per window) in a remote store.
// 1,000 requests arrive in 200 ms, split between nodes; each store call takes 1-3 ms.
function rng(seed) { // mulberry32: seeded, repeatable
return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const LIMIT = 100, N = 1000;
function run(mode) {
const rand = rng(99), ev = [];
for (let i = 0; i < N; i++) {
const t = rand() * 200, node = rand() < 0.5 ? "A" : "B";
const rtt = 1 + 2 * rand(); // read reply arrives after rtt
ev.push({ t, kind: "read", id: i, node }, { t: t + rtt, kind: "write", id: i, node });
}
ev.sort((a, b) => a.t - b.t); // the seeded interleaving
let counter = 0, admitted = 0; const seen = {}, perNode = { A: 0, B: 0 };
for (const e of ev) {
if (mode === "atomic INCR") { // one indivisible step in the store
if (e.kind === "read") { counter++; if (counter <= LIMIT) { admitted++; perNode[e.node]++; } }
continue;
}
if (e.kind === "read") { seen[e.id] = counter; continue; } // GET (value is now stale)
if (seen[e.id] >= LIMIT) continue; // decided on the stale value
admitted++; perNode[e.node]++;
if (mode === "GET then SET v+1") counter = seen[e.id] + 1; // lost updates
else counter++; // GET then INCR
}
return [mode.padEnd(18), String(admitted).padStart(9), String(admitted - LIMIT).padStart(12),
String(counter).padStart(14), ("A " + perNode.A + " / B " + perNode.B).padStart(16)].join("");
}
console.log("method admitted over limit final counter per node");
for (const m of ["GET then SET v+1", "GET then INCR", "atomic INCR"]) console.log(run(m));আউটপুট:
method admitted over limit final counter per node
GET then SET v+1 1000 900 89 A 494 / B 506
GET then INCR 114 14 114 A 50 / B 64
atomic INCR 100 0 1000 A 47 / B 53
GET তারপর SET: ১,০০০টির সবই ছাড়া পেয়েছে — সীমার ১০ গুণ — আর শেষে counter মাত্র ৮৯! প্রতি মুহূর্তে প্রায় ১০টি অনুরোধ “উড়ন্ত” (সেকেন্ডে ৫,০০০ × গড়ে ২ ms, Little-এর সূত্র); সবাই একই পুরোনো মান পড়ে একই মান লেখে, তাই counter সীমা ছোঁয়ই না।
GET তারপর INCR: counter এখন সঠিক (১১৪ = ছাড়া পাওয়া), কিন্তু সিদ্ধান্ত পুরোনো মানে, তাই ১৪টি বাড়তি — উড়ন্ত অনুরোধের সংখ্যার কাছাকাছি। Atomic INCR: ঠিক ১০০, কোনো বাড়তি নেই। Counter ১,০০০, কারণ ফেরত পাওয়ারাও গোনা হয়েছে; Lua script-এ শুধু ছাড়া পেলে বাড়ানো যায়।
একটি Lua script-এ token bucket দেখতে এমন। এটি পড়ে বোঝার জন্য; production-এ নিজের Redis সংস্করণের documentation আর পরীক্ষায় যাচাই করুন:
-- KEYS[1] = bucket key; ARGV = rate (tokens/s), burst, now (ms), cost
local rate, burst = tonumber(ARGV[1]), tonumber(ARGV[2])
local now, cost = tonumber(ARGV[3]), tonumber(ARGV[4])
local s = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(s[1]) or burst -- new key: full bucket
local ts = tonumber(s[2]) or now
tokens = math.min(burst, tokens + (now - ts) / 1000 * rate)
local allowed = 0
if tokens >= cost then tokens = tokens - cost; allowed = 1 end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(burst / rate * 1000) + 1000)
return { allowed, math.floor(tokens) } -- whole tokens leftTTL বালতি পূর্ণ হওয়ার সময়ের একটু বেশি — তারপর key না থাকলেও ফল একই, কারণ নতুন key মানে পূর্ণ বালতি। Redis Cluster-এ একটি script-এর সব key একই shard-এ থাকতে হয়; একজন user-এর কয়েকটি key একসাথে ছুঁতে হলে key-তে hash tag (যেমন rl:{u42}:min) দিয়ে তাদের একই slot-এ রাখা হয় (পর্ব ৩০)।
পথ ২ — প্রতিটি node-এ local সীমা, মাঝে মাঝে মিলিয়ে নেওয়া
প্রতিটি অনুরোধে store-এ যাওয়া মানে এক round trip বাড়তি latency, আর store-ই নতুন single point of failure. বিকল্প:
- সীমা ভাগ করা: N-টি gateway থাকলে প্রত্যেকে limit ÷ N গোনে। Network call নেই। কিন্তু load balancer একজনের অনুরোধ সমান ভাগ না করলে তিনি নিজের সীমার আগেই আটকান, আর autoscaling-এ N বদলালে ভাগও বদলাতে হয়।
- মাঝে মাঝে মেলানো: প্রতিটি node local গোনে, আর প্রতি ১০০ ms-এর মতো সময়ে নিজের গণনা কেন্দ্রে বা অন্যদের পাঠায়, সবার মোট জেনে নেয়। সীমা প্রায় ঠিক, কিন্তু সিঙ্ক-এর ফাঁকে খানিকটা বাড়তি ঢোকে।
- Token leasing: gateway কেন্দ্র থেকে একসাথে ধরা যাক ১০টি token ধার নেয়, সেগুলো নিজের memory থেকে খরচ করে, ফুরালে আবার চায়। Store-এ call প্রায় ১০ গুণ কম; দাম হলো একজনের ধার নেওয়া অব্যবহৃত token অন্য gateway-তে কাজে লাগে না।
পথ ৩ — আনুমানিক global সীমা মেনে নেওয়া
সীমা ১০০ হলে ১০৫টি ছাড়া পেলে কি ক্ষতি? ন্যায্যতার সীমায় সাধারণত না; তাই অনেক ব্যবস্থা দুই স্তর রাখে — প্রতিটি node-এ একটি দ্রুত local token bucket (ঢিলা সীমা, ঢল ঠেকাতে) আর একটি কেন্দ্রীয় সেবা (সঠিক ভাগের জন্য)। Envoy proxy-তে local rate limit filter আর বাইরের global rate limit service, দুটিই আছে (documentation অনুযায়ী; সংস্করণভেদে আলাদা)। কিন্তু টাকা বা নিরাপত্তার সীমায় — OTP, পেমেন্ট চেষ্টা, login — নিখুঁত atomic হিসাবই নিন। আর একাধিক region-এ একটি নিখুঁত global সীমা মানে প্রতিটি অনুরোধে region পেরিয়ে সমন্বয়, অর্থাৎ বহু ms বাড়তি; সাধারণত প্রতিটি region-কে মোট সীমার একটি ভাগ দেওয়া হয় (পর্ব ৪০-এর consistency-র দর-কষাকষি এখানেও)।
ঘড়ি, hot key আর store বিকল হলে
- ঘড়ি: fixed window-এর key-তে মিনিটের নাম বসে, যেমন
1015. দুই gateway-এর ঘড়িতে কয়েকশো ms ফারাক (clock skew) থাকলে সীমানার কাছে তারা দুটি আলাদা window-এ গোনে। Token bucket-এ পিছিয়ে থাকা ঘড়িরnowঋণাত্মক ব্যবধান দেখাতে পারে। প্রতিকার: সময় এক উৎস থেকে নিন — store-এর নিজের ঘড়ি (Redis-এTIME) — আর ঋণাত্মক ব্যবধান ০ ধরুন। ব্যবধান মাপতে wall clock নয়, monotonic clock (পর্ব ৩৯)। - Hot key: একটি বড় tenant-এর একটি global key-তে সেকেন্ডে লাখো call গেলে একটি Redis shard-ই গরম হয়। প্রতিকার: counter-কে কয়েকটি ভাগে ভাঙা (sharded counter), local token leasing, বা আগে বের করা কোন key-গুলো এমন — heavy hitters, যা count-min sketch দিয়ে কম memory-তে খোঁজা যায় (পর্ব ৪৫)।
- Store বিকল: limiter-এর call-এ ছোট timeout (কয়েক ms) দিন। তারপর একটি সিদ্ধান্ত: fail open (সবাইকে ছেড়ে দিন — সেবা চালু, সুরক্ষা সাময়িক বন্ধ) নাকি fail closed (সবাইকে ফেরান — সুরক্ষা আছে, সেবা নেই)। সাধারণ API-তে প্রায়ই fail open, সাথে প্রতিটি node-এ একটি ঢিলা local সীমা; OTP বা পেমেন্টে fail closed. বারবার ব্যর্থ store-কে ডাকা থামাতে circuit breaker (পর্ব ৪৮)।
৭ · কোথায় বসাবেন আর কী উত্তর দেবেন — 429, Retry-After আর RateLimit header
কোথায় — প্রতিটি স্তরের নিজের কাজ
| স্তর | কী জানে | ভালো কাজ |
|---|---|---|
| ক্লায়েন্ট (অ্যাপ, SDK) | নিজের পাঠানো | নিজেই হার মানা, header পড়ে ধীর হওয়া, retry-তে backoff; কিন্তু বিশ্বাসযোগ্য নয় — সুরক্ষা সবসময় সার্ভারে |
| Edge (CDN, WAF) | IP, দেশ, header | অচেনা বিপুল ঢল মূল সার্ভারে পৌঁছানোর আগেই ছাঁকা; DDoS-এর প্রথম প্রতিরক্ষা (পর্ব ১৭, পর্ব ৫২) |
| API gateway | পরিচয়: user, API key, tenant | ন্যায্য ভাগ, চুক্তির quota, endpoint-ভিত্তিক সীমা — এক জায়গায়, সব সার্ভিসের জন্য (পর্ব ১১) |
| সার্ভিস | ব্যবসার অর্থ | “একটি নম্বরে ১০ মিনিটে ৩টি OTP”, “একটি কার্ডে ঘণ্টায় ৫টি ব্যর্থ পেমেন্ট” — যা gateway জানে না |
| Per-tenant / dependency | ভাগ করা সম্পদ | প্রতি tenant আলাদা সীমা আর pool, database-এর connection pool — একজনের ঢল যেন বাকিদের না ডোবায় |
নিরাপত্তায় rate limit একটি স্তর, পুরো দেয়াল নয়। Login-এ শুধু IP ধরলে CGNAT-এর নির্দোষ মানুষ আটকান; শুধু account ধরে কড়া lock করলে অন্য কেউ ইচ্ছে করে ভুল password দিয়ে আসল মালিককেই বাইরে রাখতে পারেন। তাই দুটি মিলিয়ে, স্থায়ী lock-এর বদলে ধাপে ধাপে বাড়তি দেরি বা যাচাই। Authentication-এর বাকি নকশা পর্ব ৫২-এ, আর network-স্তরের প্রতিরক্ষা Computer Networks · network-এর আক্রমণ ও প্রতিরক্ষা-তে।
কী উত্তর — 429, Retry-After আর সীমার খবর
সীমা পেরোলে সঠিক উত্তর 429 Too Many Requests (পর্ব ৮-এ status code-এর তালিকা)। সাথে Retry-After — কত সেকেন্ড পরে, বা কোন সময়ের পরে, আবার চেষ্টা করা যাবে। আর ক্লায়েন্ট যাতে আগেই ধীর হতে পারে, স্বাভাবিক উত্তরেও সীমার খবর দেওয়া যায়:
HTTP/1.1 429 Too Many Requests
Retry-After: 20
RateLimit-Policy: "per-user";q=100;w=60
RateLimit: "per-user";r=0;t=20
Content-Type: application/problem+json
Cache-Control: no-store
{
"type": "https://api.shop.example/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"detail": "Limit is 100 requests per minute. Try again in about 20 seconds."
}RateLimit header নিয়ে সতর্কতা। লেখার সময়ে IETF-এর httpapi working group-এ “RateLimit header fields for HTTP” একটি খসড়া, চূড়ান্ত মানদণ্ড নয়; খসড়ার সংস্করণে নাম আর গঠন বদলেছে (আগের খসড়ায় RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset আলাদা field ছিল), আর আরও বদলাতে পারে। উপরের উদাহরণে q = সীমা, w = window সেকেন্ডে, r = বাকি, t = নবায়ন পর্যন্ত সেকেন্ড। বহু API নিজস্ব X-RateLimit-* ব্যবহার করে, যেখানে “Reset” কারও কাছে Unix সময়, কারও কাছে বাকি সেকেন্ড — client লেখার আগে সেই API-র documentation পড়ুন।
ভালো উত্তরের চারটি নিয়ম: (১) কোন নিয়ম ভেঙেছে, তা জানান, ভেতরের তথ্য নয়; (২) উত্তরটি ছোট আর সস্তা রাখুন (পর্ব ১৮); (৩) Retry-After-এর মান সত্যিকারের হিসাব থেকে দিন — GCRA-তে tat − now − tau, fixed window-এ window শেষ হতে বাকি সময়; (৪) সবাইকে একই মান দিলে সবাই একই মুহূর্তে ফেরেন — মানটিতে একটু jitter যোগ করুন।
ক্লায়েন্টের দায়িত্ব
429 পেয়ে সঙ্গে সঙ্গে আবার পাঠানো ক্লায়েন্ট নিজের সমস্যা নিজেই বাড়ায়, আর retry storm তৈরি করে। নিয়ম: Retry-After থাকলে সেটি মানুন; না থাকলে exponential backoff আর jitter; retry-র মোট সংখ্যায় সীমা; আর যে অনুরোধ এখন জরুরি নয় (analytics, prefetch), তা বাদ দিন। Backoff-এর পুরো নকশা আছে পর্ব ৪৮-এ: Resilience pattern — timeout, retry, backoff, circuit breaker আর bulkhead. দরকারে ক্লায়েন্ট নিজের ভেতরেও একটি token bucket রাখে, যাতে সীমা ছোঁয়ার আগেই নিজে ধীর হয়।
৮ · Adaptive সীমা আর ইন্টারভিউতে rate limiter নকশা
স্থির সংখ্যা যখন যথেষ্ট নয়
“প্রতি user মিনিটে ১০০” একটি স্থির সংখ্যা, load test-এর দিনের ক্ষমতা ধরে বানানো। কিন্তু পেছনের database ধীর হলে একই হার অনেক বেশি ভার। পর্ব ১৮-এর হিসাবটি মনে করুন — Little-এর সূত্র L = λ·W: সেকেন্ডে ২০০টি অনুরোধ আর প্রতিটি ০.১ সেকেন্ড মানে ভেতরে গড়ে ২০টি; latency ০.৫ সেকেন্ড হলে একই ২০০-তে ভেতরে ১০০টি। Rate limit তখনও ২০০ ঢোকাবে।
তাই দুটি adaptive কৌশল প্রচলিত: (১) concurrency limit, যা ভেতরে চলমান অনুরোধ গোনে আর latency বাড়লে AIMD-এর মতো নিয়মে নিজের সীমা কমায় — পূর্ণ আলোচনা পর্ব ১৮-এ; (২) চাপ বুঝে rate limit বদলানো: পেছনের সার্ভিস সুস্থ থাকলে প্রতিটি tenant নিজের চুক্তির সীমা পান, চাপের সংকেত (latency, ত্রুটির হার) এলে সবার সীমা একই অনুপাতে, বা কম জরুরি শ্রেণিতে আগে, কমে। ক্লায়েন্টের দিকে একই ভাবনা adaptive throttling. সবই পরিমাপ-নির্ভর, তাই limiter-এর নিজের metric লাগে (পর্ব ৫০)।
আরও গভীরে: Stripe-এর ২০১৭ সালের দিকে প্রকাশিত engineering blog “Scaling your API with rate limiters” চার ধরনের limiter পাশাপাশি চালানোর বর্ণনা দেয় — প্রতি user-এর হারের সীমা, একসাথে চলমান অনুরোধের সীমা, আর ভিড়ে কম জরুরি traffic ফেরানোর দুটি load shedder — আর Redis-এ token bucket-এর কথা বলে (প্রকাশিত বর্ণনা অনুযায়ী)। Cloudflare-এর একই সময়ের blog বিপুল সংখ্যক domain-এ sliding window counter-এর অভিজ্ঞতা লেখে। আর RFC 2697/2698-এর three-color marker দেখায়, ফেরত না দিয়ে সীমার একটু ওপরের অনুরোধকে কম অগ্রাধিকারে “চিহ্নিত” করাও একটি বিকল্প।
ইন্টারভিউতে: “একটি rate limiter নকশা করুন”
এটি system design ইন্টারভিউর সবচেয়ে চেনা প্রশ্নগুলোর একটি। পর্ব ৬-এর কাঠামো ধরে এগোন — requirement, হিসাব, API ও data model, তারপর deep dive.
- Requirement জিজ্ঞেস করুন। কার হিসাবে? কোন নিয়ম, burst কত? কয়টি gateway, কয়টি region? ১% বাড়তি চলবে? বিকল হলে fail open না closed? বাড়তি latency কত পর্যন্ত (ধরা যাক p99-এ ২ ms)?
- হিসাব। Peak-এ সেকেন্ডে কতগুলো অনুরোধ, প্রতিটিতে store-এ কয়টি call, এক window-এ কতগুলো সক্রিয় key, প্রতি key-তে কত byte (অনুশীলন ৪-এ পুরো হিসাব)।
- নিয়মের API আর data model. নিয়ম config-এ, কোডে নয়।
- Algorithm বাছাই আর কারণ। সাধারণত token bucket বা GCRA (burst সয়, state ছোট); ছোট নিরাপত্তা-সীমায় sliding log.
- Deep dive. Atomicity (Lua), sharding আর hot key, local cache বা token leasing, ঘড়ি, store বিকল হলে কী, একাধিক region, header আর ক্লায়েন্টের আচরণ, metric আর নতুন নিয়মের shadow mode.
{ "rules": [
{ "id": "user-default", "key": ["user"], "rate": 100, "per": "1m", "burst": 20 },
{ "id": "login", "key": ["user", "ip"], "rate": 10, "per": "15m" },
{ "id": "otp-sms", "key": ["phone"], "rate": 3, "per": "10m", "failMode": "closed" },
{ "id": "tenant", "key": ["tenant"], "rate": 500, "per": "1s", "burst": 1000 }
] }
Redis: rl:{u42}:user-default -> hash { tokens: 17.4, ts: 1761638400123 } TTL ~ 2 min
rl:{8801XXXXXXXXX}:otp-sms -> sorted set of timestamps (exact, small)পরীক্ষা ও ইন্টারভিউতে (বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় প্রায়ই যে ধরনের প্রশ্ন আসে):
- Token bucket আর leaky bucket-এর পার্থক্য? — Token bucket b পর্যন্ত burst ছাড়ে, ছাড়া ≤ b + r·T; leaky queue বেরোনো স্রোত সমান রাখে কিন্তু অপেক্ষা যোগ করে।
- Fixed window-এর সমস্যা? — দুই window-এর সীমানায় অল্প সময়ে সীমার দ্বিগুণ ঢুকতে পারে।
- Sliding window counter কীভাবে হিসাব করে? — আগের গণনা × (1 − f) + এখনকার গণনা; আগের window-এ সমান ছড়ানো অনুমান।
- বহু সার্ভারে সীমা ঠিক রাখবেন কীভাবে? — ভাগ করা store-এ atomic INCR বা Lua script; নইলে race-এ সীমা ফুটো হয়।
- সীমা পেরোলে কী উত্তর? — 429, Retry-After, আর ক্লায়েন্টে backoff ও jitter.
৯ · AI-এর যুগে rate limiter-এর নকশা যাচাই
AI-কে rate limiter চাইলে কয়েক সেকেন্ডে Redis আর token bucket-এর কোড আসে, প্রায়ই সাথে “Kafka আর microservice যোগ করুন”। এক সার্ভারে কোডটি ঠিক চলতেও পারে; কিন্তু কার হিসাব, কয়টি gateway, store বিকল হলে কী — এসব প্রায়ই বাদ যায়। সাধারণ ভুলগুলো:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| GET, তারপর হিসাব, তারপর SET — আলাদা call-এ | বহু gateway-তে race; অংশ ৬-এ সীমার ১০ গুণ পর্যন্ত | প্রতিটি read-modify-write atomic কি না (Lua, INCR) খুঁজুন; race simulator চালান |
| INCR আর EXPIRE আলাদা, মাঝে crash ভাবা হয়নি | TTL-হীন key — ব্যবহারকারী চিরতরে আটকান | Failure-mode পর্যালোচনা: প্রতিটি দুই-ধাপের কাজের মাঝে থামলে কী থাকে? |
| প্রতিটি instance-এ memory-তে গণনা | N-টি instance মানে কার্যত N গুণ সীমা | Instance-সংখ্যা জিজ্ঞেস করুন; সীমা ÷ N বা ভাগ করা store |
| শুধু IP দিয়ে কড়া সীমা | NAT আর CGNAT-এ নির্দোষ মানুষ আটকান | Login-এর পরের key আছে কি না; IP-সীমা ঢিলা কি না |
| Sliding window counter-কে “নিখুঁত” বলা | এটি অনুমান, দুই দিকেই ভুল হয় | অংশ ৪-এর B আর C পরিস্থিতি নিজে চালান |
| Memory আর QPS-এর ভুল অঙ্ক, পণ্যের default বা header-এর নাম বানিয়ে বলা | এক শূন্যের ভুলে ১০ গুণ; header খসড়া, সংস্করণভেদে আলাদা | অঙ্ক নিজে আবার করুন; অফিসিয়াল documentation দেখুন |
AI-কে সংখ্যা, পরিবেশ আর ব্যর্থতার প্রশ্ন দিয়ে বাঁধুন:
AI-এর rate limiter হাতে পেলে — যাচাইয়ের চেকলিস্ট
অঙ্ক নিজে মেলান, race আর সীমানা চালিয়ে দেখুন, ব্যর্থতার পথ ভাবুন।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। Algorithm-এর নাম মুখস্থ নয়, কোন ভুল কোথায় সহনীয় তা চেনাই আসল লক্ষ্য।
প্র ০১একই নাম “leaky bucket” কেন দুটি আলাদা জিনিস বোঝায়, আর এই গুলিয়ে ফেলা বাস্তবে কী সমস্যা তৈরি করে?
নামটির দুই উৎস। Network-এর পাঠ্যবইয়ে (যেমন Andrew Tanenbaum-এর Computer Networks) leaky bucket প্রায়ই একটি queue হিসেবে আঁকা হয়: packet বালতিতে জমে, নিচের ফুটো দিয়ে স্থির হারে বেরোয়, বালতি ভরলে নতুনটি পড়ে যায়। অন্যদিকে ১৯৯০-এর দশকের ATM network-এর মানদণ্ডে GCRA-কে leaky bucket-এর “meter” রূপ হিসেবে বর্ণনা করা হয়: কিছুই অপেক্ষা করে না, শুধু একটি কাল্পনিক স্তর মাপা হয়, আর যে cell স্তর উপচায় সেটি তখনই বাতিল বা চিহ্নিত হয়।
গণিতে meter রূপটি token bucket-এর আয়না: বালতির “পানি” হলো খরচ হওয়া token, ফুটো হলো token জমার হার। তাই একই r আর b দিলে meter আর token bucket প্রতিটি সিদ্ধান্তে মেলে — অংশ ৩-এর stepper-এ GCRA-র সারি তা দেখায়। Queue রূপটি সম্পূর্ণ আলাদা: সে অনুরোধ ফেরায় কম, কিন্তু দেরি করায়। অংশ ৩-এর simulator-এ queue-র শেষজন প্রায় ২ সেকেন্ড অপেক্ষা করেছেন।
বাস্তবে এই গুলিয়ে ফেলা দুইভাবে ক্ষতি করে। প্রথমত, একটি দল “leaky bucket চাই” লিখে queue বানাল, আর interactive API-র p99 latency হঠাৎ কয়েক সেকেন্ড বেড়ে গেল — অথচ কেউ 429 দেখছে না, তাই সমস্যাটি চোখেও পড়ে না। দ্বিতীয়ত, উল্টোটা: একটি পেছনের ব্যবস্থা (ধরা যাক SMS পাঠানোর বাইরের সেবা) কঠোরভাবে সেকেন্ডে ১০টি চায়, দল meter বসাল, আর burst-এর ২০টি একসাথে চলে গিয়ে বাইরের সেবা থেকে নিজেরাই 429 খেল। Nginx-এর limit_req এর চেনা উদাহরণ: documentation পদ্ধতিটিকে leaky bucket বলে, burst দিলে বাড়তিরা অপেক্ষা করে, আর nodelay যোগ করলে আচরণ meter-এর মতো।
মূল উপলব্ধি: Shaping (queue) আর policing (meter) আলাদা লক্ষ্য পূরণ করে — একটি পেছনের ব্যবস্থাকে মসৃণ স্রোত দেয়, অন্যটি সামনের ব্যবহারকারীকে দ্রুত উত্তর দেয়। নামের বদলে আচরণ দিয়ে চুক্তি লিখুন।
প্র ০২IP ঠিকানা দিয়ে rate limit করা এত জনপ্রিয়, তবু বহু প্রকৌশলী একে বিপজ্জনক বলেন কেন? ভালো বিকল্প কী?
IP জনপ্রিয় কারণ এটি সবচেয়ে আগে আর সবচেয়ে সস্তায় জানা যায়: TCP সংযোগ হওয়ামাত্র, কোনো token যাচাই বা database ছাড়াই। অচেনা কোনো উৎস থেকে বিপুল অনুরোধ এলে IP-ই একমাত্র হাতল। কিন্তু “একটি IP = একজন মানুষ” ধারণাটি আজকের ইন্টারনেটে প্রায়ই ভুল, দুই দিক থেকেই।
প্রথম দিক — অনেক মানুষ, এক IP. বাসা আর অফিসের router-এ NAT, আর মোবাইল অপারেটরের CGNAT-এ হাজারো গ্রাহক কয়েকটি public ঠিকানায়। IPv4 ঠিকানা ফুরিয়ে আসার পরে এটি এত সাধারণ হয়েছে যে RFC 6598 (২০১২) CGNAT-এর জন্য আলাদা একটি ঠিকানা-পরিসর (100.64.0.0/10) সংরক্ষণ করেছে। এখানে IP-ভিত্তিক কড়া সীমা নির্দোষ মানুষকে আটকায়, আর অভিযোগ আসে “অ্যাপ কাজ করছে না”।
দ্বিতীয় দিক — এক মানুষ, অনেক IP. অপব্যবহারকারী বহু ঠিকানা বদলাতে পারেন, আর IPv6-এ একজন সাধারণ গ্রাহকই প্রায়ই অন্তত একটি পুরো /64 পরিসর পান — কোটি কোটি ঠিকানা। একক IPv6 ঠিকানা গুনলে সীমা অর্থহীন; তাই prefix ধরে গোনা প্রচলিত।
ভালো নকশা স্তরে স্তরে: IP বা prefix-এর ঢিলা সীমা edge-এ, শুধু অস্বাভাবিক ঢল ঠেকাতে; তারপর পরিচয় জানার পরে user, API key আর tenant-এর আসল সীমা। Login-এর আগের কাজে (login, sign-up, OTP) পরিচয় নেই, তাই সেখানে কয়েকটি সংকেত মিলিয়ে — account, ফোন নম্বর, IP prefix, ডিভাইসের সংকেত — আর কড়া “না”-এর বদলে ধাপে ধাপে বাড়তি দেরি বা অতিরিক্ত যাচাই।
মূল উপলব্ধি: IP একটি মোটা দাগের সংকেত, পরিচয় নয়। ন্যায্য ভাগ মাপুন পরিচয় দিয়ে, আর IP রাখুন শুধু অচেনা ঢলের প্রথম ছাঁকনি হিসেবে।
প্র ০৩Rate limiter-এর store বিকল হলে fail open না fail closed — প্রশ্নটির সঠিক উত্তর কেন “নিয়ম অনুযায়ী আলাদা”?
Limiter প্রতিটি অনুরোধের পথে বসে, তাই সে নিজেই একটি dependency. Redis cluster ধীর বা বিকল হলে দুটি মন্দ বিকল্প: সবাইকে ছেড়ে দেওয়া (fail open) — সেবা চলে, কিন্তু সুরক্ষা আর খরচের হিসাব সাময়িক বন্ধ; অথবা সবাইকে ফেরানো (fail closed) — সুরক্ষা থাকে, কিন্তু একটি সহায়ক অংশের বিভ্রাট পুরো সেবার বিভ্রাট হয়ে যায়। পর্ব ৫-এর availability-র অঙ্কে, পথে প্রতিটি নতুন serial অংশ মোট availability কমায়; fail closed limiter ঠিক তেমন একটি অংশ।
তাই প্রশ্নটি নিয়ম ধরে করুন: “এই সীমা কয়েক মিনিট না থাকলে সবচেয়ে খারাপ কী হতে পারে?” সাধারণ পাতা দেখা বা feed-এর ন্যায্য ভাগের সীমা কয়েক মিনিট না থাকলে কিছু ভারী ব্যবহারকারী বেশি পাবেন — বিরক্তিকর, কিন্তু সেবা বন্ধের চেয়ে অনেক ভালো। তাই fail open, সাথে প্রতিটি gateway-তে একটি ঢিলা local token bucket, যাতে store না থাকলেও একজনের ঢল পুরো সিস্টেম না বসায়।
অন্যদিকে OTP-র SMS: প্রতিটি বার্তায় টাকা লাগে, আর সীমা না থাকলে অল্প সময়ে বিপুল বার্তা পাঠানো যায় — বড় বিল, আর মানুষের ফোনে অনাকাঙ্ক্ষিত বার্তা। পেমেন্টের ব্যর্থ চেষ্টা আর login-এর সীমাও নিরাপত্তার। এগুলো fail closed, অথবা অন্তত খুব কড়া local সীমায় নামা। অনেক দল নিয়মের config-এই প্রতিটি নিয়মের failMode লিখে রাখে — অংশ ৮-এর উদাহরণের মতো — যাতে বিভ্রাটের রাতে কেউ তাড়াহুড়ায় সিদ্ধান্ত না নেন।
আর fail open হলে সেটি alert আর metric-এ স্পষ্ট দেখান (পর্ব ৫০) — নীরবে সুরক্ষাহীন চলাই সবচেয়ে বিপজ্জনক।
মূল উপলব্ধি: Limiter-এর ব্যর্থতার নীতি নিয়মের ক্ষতির ধরন থেকে আসে: ন্যায্যতার সীমা খোলে, খরচ আর নিরাপত্তার সীমা বন্ধ হয় — আর দুটিই আগে থেকে লেখা থাকে।
প্র ০৪একটি নিখুঁত global rate limit কি সবসময় চাওয়ার মতো? একাধিক region-এ এর দাম কী?
নিখুঁত মানে: পৃথিবীর যেকোনো gateway-তে আসা অনুরোধ মিলিয়ে কখনো সীমার একটিও বেশি নয়। এর জন্য প্রতিটি সিদ্ধান্তের আগে সব gateway-কে একটি একক, সর্বশেষ গণনা দেখতে হবে — অর্থাৎ counter-টি linearizable হতে হবে (পর্ব ৪০)। এক data center-এ একটি Redis shard এটি সস্তায় দেয়: একটি atomic অপারেশন, প্রায় এক ms. কিন্তু ব্যবহারকারী যদি ঢাকা, সিঙ্গাপুর আর ফ্রাঙ্কফুর্টের region থেকে সেবা পান, তবে প্রতিটি অনুরোধে অন্য মহাদেশের counter-এ যাওয়া মানে বহু দশ বা শত ms বাড়তি — আর সেই region বিচ্ছিন্ন হলে হয় সীমা বন্ধ, নয়তো সেবা বন্ধ। CAP-এর দর-কষাকষি এখানে খুব বাস্তব।
তাই বাস্তব নকশা প্রায়ই নিখুঁততা ছেড়ে দেয়, জেনে-বুঝে। একটি পথ: মোট সীমা region-গুলোর মধ্যে ভাগ (যেমন সাম্প্রতিক traffic-এর অনুপাতে), প্রতিটি region নিজের ভাগে নিখুঁত, আর কয়েক সেকেন্ড পরপর ভাগ বদলানো। আরেক পথ: প্রতিটি region local-এ গোনে আর নিয়মিত গণনা ছড়িয়ে দেয়, ফলে সীমা eventually ঠিক হয়, মাঝখানে সামান্য বাড়তি ঢোকে।
সিদ্ধান্তের মাপকাঠি ব্যবসা: বাড়তি ৫% অনুরোধে ক্ষতি কী? ন্যায্যতা আর ক্ষমতা-রক্ষার সীমায় সাধারণত সামান্য — পেছনের সার্ভিসের তো headroom আছেই (পর্ব ৪-এর ক্ষমতার পরিকল্পনা)। চুক্তির quota-য়, যেখানে টাকা জড়িত, দিন বা মাসের শেষে নিখুঁত হিসাব লাগে, কিন্তু সেটা পরে log থেকেও মেলানো যায়; প্রতিটি অনুরোধে নয়। আর নিরাপত্তার ছোট সীমা — একটি নম্বরে ৩টি OTP — সাধারণত একটি region-এর ভেতরেই থাকে, কারণ একটি নম্বরের অনুরোধ এক জায়গায় পাঠানো যায় — key ধরে routing.
মূল উপলব্ধি: নিখুঁত global সীমা মানে প্রতিটি অনুরোধে সমন্বয়; বেশিরভাগ সীমায় সামান্য, পরিমাপযোগ্য ভুল মেনে নেওয়াই ভালো প্রকৌশল — আর যেখানে টাকা বা নিরাপত্তা, সেখানে নিখুঁততা ছোট পরিসরে কিনুন।
প্র ০৫“প্রতি user মিনিটে ১০০” — সংখ্যাটি কোথা থেকে আসে? ভুল সংখ্যা কীভাবে নিজেই বিভ্রাট তৈরি করতে পারে?
সংখ্যাটির তিনটি উৎস থাকা উচিত, অনুমান নয়। প্রথম, স্বাভাবিক ব্যবহার: আসল traffic-এর log থেকে প্রতি user প্রতি মিনিটে অনুরোধের বণ্টন — p50, p99, p99.9. সীমা বসে p99.9-এর বেশ ওপরে, যাতে সাধারণ মানুষ কখনো না ছোঁন; burst b আসে অ্যাপ খোলার মুহূর্তের ঝাঁকুনি থেকে। দ্বিতীয়, ক্ষমতা: load test-এ পেছনের সার্ভিস কত নেয়, আর সবচেয়ে খারাপ ক্ষেত্রে কতজন একসাথে সীমায় থাকতে পারেন। তৃতীয়, চুক্তি আর খরচ: পার্টনারের প্যাকেজ, প্রতিটি SMS বা বাইরের API call-এর দাম।
ভুল সংখ্যা দুই দিকেই বিভ্রাট আনে। খুব কড়া হলে একটি নতুন অ্যাপ সংস্করণ — যে পাতা খোলার সময় আগের চেয়ে দ্বিগুণ অনুরোধ করে — হঠাৎ লাখো মানুষকে 429 দেখায়। সার্ভার পুরো সুস্থ, তবু ব্যবহারকারীর চোখে সেবা বন্ধ। আরও খারাপ, ক্লায়েন্ট যদি 429-এ সঙ্গে সঙ্গে retry করে, সীমা নিজেই একটি retry storm তৈরি করে। খুব ঢিলা হলে সীমা থেকেও নেই — একটি bug-ওয়ালা script পুরো database দখল করে।
তাই নতুন নিয়ম সরাসরি চালু না করে প্রথমে shadow mode-এ: নিয়ম হিসাব রাখে আর log করে “এই অনুরোধ আটকাত”, কিন্তু আটকায় না। এক-দুই সপ্তাহের তথ্যে দেখা যায় কতজন, কোন সময়, কোন অ্যাপ সংস্করণ সীমা ছুঁত। তারপর ধাপে ধাপে চালু — একটি ছোট অংশে, ঠিক যেমন পর্ব ৫৩-এর canary release. আর প্রতিটি নিয়মের metric — ছাড়া, ফেরত, কোন key সবচেয়ে বেশি ফেরত — dashboard-এ রাখুন (পর্ব ৫০), যাতে অ্যাপের নতুন সংস্করণের পরে ফেরতের হার লাফালে সাথে সাথে চোখে পড়ে।
মূল উপলব্ধি: সীমার সংখ্যা একটি পরিমাপ-ভিত্তিক নকশা-সিদ্ধান্ত: log থেকে স্বাভাবিক ব্যবহার, load test থেকে ক্ষমতা, shadow mode-এ যাচাই, তারপর ধাপে ধাপে চালু।
অনুশীলন
কাগজ-কলমে হিসাব করুন, তারপর উত্তর মেলান। তৃতীয় প্রশ্নে আগে অনুমান, তারপর কোড চালিয়ে যাচাই।
-
সীমার হিসাব: (ক) একটি token bucket-এ r = সেকেন্ডে ৫, b = ৫০, আর বালতি শুরুতে পূর্ণ। একজন ক্লায়েন্ট t = ০ থেকে প্রতি ০.০৫ সেকেন্ডে একটি করে মোট ২০০টি অনুরোধ পাঠান (সেকেন্ডে ২০টি, শেষটি t = ৯.৯৫ সেকেন্ডে)। কতগুলো ছাড়া পাবে, আর প্রথম 429 মোটামুটি কখন? (খ) মিনিটে ১০০-র fixed window-এ অল্প সময়ে সর্বোচ্চ কতগুলো ঢুকতে পারে, আর কখন? (গ) একই সীমার sliding window counter-এ আগের window-এ ৯০টি, এখনকারটিতে ৩০টি, আর এখনকার window-এর ২০ সেকেন্ড পেরিয়েছে। এই মুহূর্তে একসাথে কতগুলো নতুন অনুরোধ ছাড়া পাবে?
- (ক) ছাদ: b + r·T = ৫০ + ৫ × ৯.৯৫ = ৯৯.৭৫, তাই সর্বোচ্চ ৯৯টি। বালতি খালি হয় কখন: t সেকেন্ডে token প্রায় ৫০ + ৫t − ২০t = ৫০ − ১৫t; এটি ১-এর নিচে নামে t ≈ ৩.২৭ সেকেন্ডের পরে, তাই প্রথম 429 প্রায় ৩.৩ সেকেন্ডে। একটি ছোট script-এ চালিয়ে মেলানো: ছাড়া ৯৯, প্রথম ফেরত t = ৩.৩০।
- (খ) ২০০টি, সীমানার দুই পাশে: এক window-এর শেষ মুহূর্তে ১০০, পরেরটির শুরুতে ১০০ — দুই সেকেন্ডেরও কম সময়ে।
- (গ) ওজন 1 − ২০/৬০ = ২/৩, তাই অনুমান = ৯০ × ২/৩ + ৩০ = ৬০ + ৩০ = ৯০। নতুন অনুরোধ ছাড়া পায় যতক্ষণ ৬০ + গণনা < ১০০, অর্থাৎ গণনা ৩৯ পর্যন্ত: ৩০, ৩১, … ৩৯ — ১০টি। এগারোতমটিতে অনুমান ১০০, তাই 429.
-
একটি নকশা সমালোচনা করুন: একটি কাল্পনিক mobile banking সেবার দল লিখেছে: “login API-তে প্রতি IP মিনিটে ৬০টি অনুরোধ; গণনা প্রতিটি gateway-এর memory-তে (৫টি gateway); fixed window; সীমা পেরোলে 503; Redis লাগবে না।” অন্তত পাঁচটি সমস্যা খুঁজুন আর একটি ভালো নকশা দিন।
- শুধু IP: CGNAT-এর পেছনে একই IP-র শত মানুষ একসাথে আটকাতে পারেন, আর অপব্যবহারকারী IP বদলে ফাঁক গলেন।
- প্রতিটি gateway-তে আলাদা গণনা: ৫টি gateway মানে কার্যত মিনিটে ৩০০ পর্যন্ত, আর load balancer কাকে কোথায় পাঠায় তার ওপর ফল নির্ভর করে।
- Fixed window: সীমানায় ২ সেকেন্ডে ১২০ পর্যন্ত (৫ gateway মিলে ৬০০)।
- 503: সমস্যা ক্লায়েন্টের সীমা, সার্ভারের নয় — 429 আর Retry-After দিতে হবে। 503-এ monitoring অকারণে বিভ্রাট দেখায়।
- Login-এর আসল ঝুঁকি ধরা হয়নি: বারবার ভুল password-এর সীমা account ধরে লাগে, শুধু অনুরোধ-সংখ্যা নয়; আর store বিকল হলে কী হবে (fail closed?) লেখা নেই।
একটি ভালো নকশা: edge-এ IP prefix ধরে ঢিলা সীমা; login-এ
accountধরে ১৫ মিনিটে ১০টি ব্যর্থ চেষ্টা আরaccount + IP prefixধরে আলাদা সীমা, sliding log-এ (ছোট সীমা, নিখুঁত চাই); ভাগ করা Redis-এ Lua script, TTL-সহ; ভাঙলে 429, Retry-After, আর স্থায়ী lock-এর বদলে ধাপে ধাপে বাড়তি দেরি বা অতিরিক্ত যাচাই; store বিকল হলে এই নিয়মে fail closed অথবা খুব কড়া local সীমা; আর নতুন নিয়ম প্রথমে shadow mode-এ। -
আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে? তারপর
b = 3-কেb = 1করলে কী ছাপবে? আগে কাগজে হিসাব, তারপর অংশ ৩-এর কোডের জায়গায় বসিয়ে চালিয়ে যাচাই করুন।const r = 1, b = 3; // 1 token per second, bucket of 3 let tokens = b, last = 0; const out = []; for (const t of [0, 0, 0, 0, 0.5, 1.0, 1.5, 4.0, 4.0, 4.0, 4.0]) { tokens = Math.min(b, tokens + (t - last) * r); last = t; if (tokens >= 1) { tokens -= 1; out.push("OK"); } else out.push("NO"); } console.log(out.join(" "));b = ৩-এ আউটপুট:
OK OK OK NO NO OK NO OK OK OK NO- t = ০: ৩টি token, প্রথম তিনটি OK, চতুর্থটি NO; token ০।
- t = ০.৫: ০.৫টি token — একটি পুরো token নেই, NO.
- t = ১.০: ০.৫ + ০.৫ = ১, OK; token ০।
- t = ১.৫: ০.৫, NO.
- t = ৪.০: ০.৫ + ২.৫ = ৩ (ছাদও ৩), তিনটি OK, চতুর্থটি NO.
b = ১-এ আউটপুট:
OK NO NO NO NO OK NO OK NO NO NOগড় হার একই (সেকেন্ডে ১), কিন্তু burst-এর ছাদ ১: t = ৪.০-এ আড়াই সেকেন্ড অলস থাকার পরেও মাত্র একটি OK. সময়গুলো ০.৫-এর গুণিতক, কারণ ০.৫ binary-তে নিখুঁত; ০.২-এর মতো মানে floating point-এর ভুলে একটি OK হারাতে পারত — তাই বাস্তব limiter-এ সময় প্রায়ই integer millisecond-এ।
-
ইন্টারভিউর হিসাব আর data model: একটি কাল্পনিক public API-র জন্য rate limiter নকশা করুন। ধরা যাক peak-এ সেকেন্ডে ৫ লাখ অনুরোধ, প্রতিটি অনুরোধে Redis-এ একটি Lua script call; এক মিনিটে সক্রিয় ৫০ লাখ user, প্রত্যেকের দুটি নিয়ম (সাধারণ আর endpoint-ভিত্তিক); প্রতি key-তে state আর overhead মিলিয়ে ধরা যাক ১০০ byte; প্রতিটি call-এ অনুরোধ আর উত্তর মিলিয়ে ধরা যাক ২০০ byte network; আর ধরা যাক একটি Redis shard সেকেন্ডে প্রায় ১ লাখ এমন script নিরাপদে চালায়, ৭০%-এর বেশি ব্যস্ত রাখতে চান না। কয়টি shard, কত memory, কত network? Key-এর নকশা কেমন হবে?
- Shard: ৫,০০,০০০ ÷ ১,০০,০০০ = ৫টি পুরো ব্যস্ত shard; ৭০% সীমায় ৫ ÷ ০.৭ ≈ ৭.১৪, তাই ৮টি shard. প্রতি shard-এর ক্ষমতার অনুমানটিই সবচেয়ে অনিশ্চিত — নিজের hardware আর script-এ load test করে নিন।
- Memory: ৫০ লাখ × ২ = ১ কোটি key × ১০০ byte = প্রায় ১ GB; প্রতিটি shard-এ গড়ে প্রায় ১২৫ MB. প্রতিটির একটি replica রাখলে মোট প্রায় ২ GB — memory সমস্যা নয়, QPS-ই নকশা ঠিক করে।
- Network: ৫,০০,০০০ × ২০০ byte = ১০০ MB/s ≈ ০.৮ Gbit/s, ৮টি shard-এ ভাগ হয়ে প্রতিটিতে প্রায় ১০০ Mbit/s.
- Key:
rl:{user}:rule— hash tag-এ user, তাই একজনের সব নিয়ম একই shard-এ, আর একটি script-এ দুটি নিয়ম একসাথে যাচাই করা যায়। Value: token bucket-এর{tokens, ts}hash, TTL প্রায় বালতি পূর্ণ হওয়ার সময়ের একটু বেশি। - আরও যা বলবেন: store-এর ভার কমাতে token leasing; বিকল হলে নিয়ম অনুযায়ী fail open বা closed; বড় tenant-এর global key একটি hot key — sharded counter বা leasing.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমইন্টারভিউতে “একটি rate limiter নকশা করুন” প্রশ্নের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ❓ Requirement জিজ্ঞেস করা — কার হিসাবে, কোন নিয়ম, কতটা নিখুঁত, কয়টি gateway
- 🧮 Peak QPS, সক্রিয় key আর memory-র হিসাব
- 🗂️ নিয়মের config আর store-এ key ও data model
- 🪣 Algorithm বাছাই — token bucket, GCRA বা sliding log — কারণসহ
- 🔒 Deep dive — atomic Lua, hot key, ঘড়ি, store বিকল হলে fail open বা closed
একটি token bucket-এ r = সেকেন্ডে ১০টি token আর b = ৫০, আর অনেকক্ষণ কোনো অনুরোধ আসেনি। যেকোনো ৫ সেকেন্ডের মধ্যে সর্বোচ্চ কতগুলো অনুরোধ ছাড়া পেতে পারে?
- ৫০
- ১০০
- ৬০
- ২৫০
একটি API-তে প্রতি user মিনিটে ১০০ অনুরোধের fixed window counter আছে, window শুরু হয় প্রতিটি পূর্ণ মিনিটে। একজন user দুই সেকেন্ডের কম সময়ে সর্বোচ্চ কতগুলো অনুরোধ সফলভাবে করাতে পারেন?
- ১০০
- প্রায় ৩
- ২০০
- ৩০০
মিনিটে ১০০-র sliding window counter: আগের মিনিটে ৮০টি, এখনকার মিনিটে এ পর্যন্ত ৩০টি, আর এখনকার মিনিটের ১৫ সেকেন্ড পেরিয়েছে। নতুন একটি অনুরোধের কী হবে?
- ছাড়া পাবে, কারণ অনুমান ৯০
- ফেরত পাবে, কারণ গণনা ১১০
- ছাড়া পাবে, কারণ অনুমান ৫০
- হিসাব করা যাবে না, timestamp লাগবে
দুটি gateway একটি ভাগ করা Redis counter-এ প্রতিটি অনুরোধে আগে GET করে মান দেখে, সীমার নিচে হলে তারপর আলাদা একটি call-এ INCR করে। ভারী traffic-এ কী ঘটবে?
- কিছুই না, INCR atomic বলে সব ঠিক
- Counter-এর মান বৃদ্ধি হারাবে, কিন্তু ছাড়া পাওয়ার সংখ্যা ঠিক থাকবে
- সীমার চেয়ে অনেক কম অনুরোধ ছাড়া পাবে
- সীমার চেয়ে কিছু বেশি অনুরোধ ছাড়া পাবে — check-then-act race
একটি অ্যাপের বেশিরভাগ ব্যবহারকারী মোবাইল ডেটায়, আর অপারেটর CGNAT ব্যবহার করে। Login-এর পরের API-তে ন্যায্য ভাগের সীমার জন্য সবচেয়ে ভালো key কোনটি?
- Public IP ঠিকানা
- User ID (প্রয়োজনে tenant-এর সাথে মিলিয়ে)
- ব্রাউজার বা অ্যাপের User-Agent
- শুধু endpoint-এর নাম
একটি বাইরের SMS সেবা কঠোরভাবে সেকেন্ডে ১০টির বেশি অনুরোধ নেয় না। আপনার সার্ভিসে মাঝে মাঝে একসাথে ৫০টি বার্তা তৈরি হয়, আর কয়েক সেকেন্ড দেরি গ্রহণযোগ্য। কোন পদ্ধতি সবচেয়ে উপযুক্ত?
- b = ৫০-এর token bucket, বাড়তিরা সঙ্গে সঙ্গে 429
- মিনিটে ৬০০-র fixed window
- Leaky bucket queue — সেকেন্ডে ১০টি সমান তালে পাঠানো
- Fail closed নীতি
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Rate limit একটি নীতি — ন্যায্যতা, ক্ষমতা রক্ষা, খরচ আর অপব্যবহার থেকে সুরক্ষা; সার্ভার অলস থাকলেও খাটে, উত্তর 429. Load shedding ভিড়ের প্রতিক্রিয়া, উত্তর 503.
- Key আগে, algorithm পরে: user, API key, tenant, endpoint — প্রায়ই কয়েকটি মিলিয়ে, খরচ ধরে। IP শুধু ঢিলা, মোটা দাগে, কারণ NAT আর CGNAT; IPv6-এ prefix.
- Token bucket-এ ছাড়া ≤ b + r·T; leaky queue মসৃণ কিন্তু দেরি করায়; GCRA একটি সংখ্যায় token bucket. Fixed window সীমানায় ২×, sliding log নিখুঁত কিন্তু memory-খেকো, sliding counter কাছাকাছি অনুমান।
- বহু gateway-তে atomic হিসাব (INCR, Lua), প্রতিটি key-এ TTL, এক উৎসের ঘড়ি; local সীমা বা token leasing-এ কম call, সামান্য ভুল। Store বিকল হলে নিয়ম অনুযায়ী fail open বা closed.
- উত্তরে 429, সত্যিকারের Retry-After আর (খসড়া মেনে) সীমার header; ক্লায়েন্টে backoff ও jitter. সংখ্যা আসে log আর load test থেকে, চালু হয় shadow mode পেরিয়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫০ · Observability — log, metric, trace, SLI-SLO আর error budgetপরের পর্বLimiter-এর ছাড়া-ফেরতের metric আর alert কীভাবে সাজাবেন।
- পর্ব ৪৮ · Resilience pattern — timeout, retry, backoff, circuit breaker আর bulkheadএই মডিউলে429 পেলে ক্লায়েন্ট কীভাবে backoff আর jitter দিয়ে ফেরে।
- পর্ব ১৮ · Overload সামলানো — queueing, backpressure, load shedding আর graceful degradationআগে দেখা429 বনাম 503, concurrency limit আর AIMD-এর পূর্ণ আলোচনা।
- পর্ব ১১ · Reverse proxy, API gateway আর BFF — সার্ভারের সামনের দরজাআগে দেখাGateway-তে rate limit কোথায় বসে, আর Nginx-এর limit_req config.
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Computer Networks · TCP congestion controlপাশের ট্র্যাকNetwork-এর হার নিয়ন্ত্রণ আর AIMD-এর আসল উৎস।
- DSA · Two pointers আর sliding windowগভীরে যানSliding window log-এর পেছনের algorithm-এর কৌশল।