রেট লিমিটিং (Rate Limiting) — ফান্ডামেন্টাল থেকে মাস্টার লেভেল
Table of Contents
সূচিপত্র
| পর্ব | বিষয় | লেভেল |
|---|---|---|
| ০ | এই ডকুমেন্ট কীভাবে পড়বেন | — |
| ১ | ফান্ডামেন্টাল — রেট লিমিট কী ও কেন | Foundation |
| ২ | রেট লিমিটারের অ্যানাটমি — key, cost, policy | Foundation |
| ৩ | ৬টি কোর অ্যালগরিদম (গণিতসহ) | Core |
| ৪ | ডিস্ট্রিবিউটেড রেট লিমিটিং | Core |
| ৫ | কোন লেয়ারে বসাবেন | Core |
| ৬ | HTTP কন্ট্রাক্ট — 429, হেডার, ব্যাকঅফ | Core |
| ৭ | রিয়েল ইমপ্লিমেন্টেশন (FastAPI / Django / Nginx) | Advanced |
| ৮ | অ্যাডভান্সড প্যাটার্ন | Advanced |
| ৯ | ১৫টি রিয়েল-ওয়ার্ল্ড ইউজকেস — সম্পূর্ণ working approach | Advanced |
| ১০ | বাইপাস ও সিকিউরিটি | Master |
| ১১ | অ্যান্টি-প্যাটার্ন | Master |
| ১২ | টেস্টিং ও মনিটরিং | Master |
| ১৩ | ডিজাইন ডিসিশন চিটশিট | Master |
| ১৪ | মাস্টারি রোডম্যাপ + ইন্টারভিউ প্রশ্ন | Master |
| ১৫ | কনসলিডেশন এক্সারসাইজ | — |
পর্ব ০ — এই ডকুমেন্ট কীভাবে পড়বেন
প্রতিটি পর্বে থাকছে:
- কনসেপ্ট + অ্যানালজি — জিনিসটা আসলে কী
- ASCII ডায়াগ্রাম — কীভাবে কাজ করে
- নিউমেরিক উদাহরণ — আসল হিসাব, ধরে নেওয়া কিছু নয়
- প্রোডাকশন ইনসাইট — আপনার Django
aggameপ্রজেক্টের@rate_limit/budget.reserveসিস্টেমের সাথে মিলিয়ে - Active recall প্রশ্ন — নিজেকে যাচাই
এক বসায় শেষ করার চেষ্টা করবেন না। পর্ব ১–৩ একদিন, পর্ব ৪–৭ আরেকদিন, পর্ব ৮–৯ তৃতীয় দিন, পর্ব ১০–১৫ চতুর্থ দিন — এই ভাগে পড়লে ধরে রাখা সহজ হবে।
পর্ব ১ — ফান্ডামেন্টাল
১.১ রেট লিমিট আসলে কী?
সংজ্ঞা: রেট লিমিটিং হলো এমন একটা মেকানিজম যেটা নির্দিষ্ট একটা সময়-জানালার (time window) ভেতরে কোনো ক্লায়েন্ট (identity) কতবার কোনো রিসোর্স ব্যবহার করতে পারবে — তা নিয়ন্ত্রণ করে, এবং সীমা ছাড়ালে রিকোয়েস্ট reject / delay / degrade করে।
অ্যানালজি — মেট্রোরেলের গেট:
স্টেশনে ১০টা গেট আছে। প্রতিটা গেট দিয়ে সেকেন্ডে ১ জন ঢুকতে পারে। ভিড়ের সময় ৫০০ জন একসাথে এলে কী হয়?
- গেট না থাকলে → সবাই একসাথে প্ল্যাটফর্মে, প্ল্যাটফর্ম ভেঙে পড়ে (= সার্ভার ক্র্যাশ)
- গেট থাকলে → লাইন হয়, ধীরে ধীরে ঢোকে, প্ল্যাটফর্ম নিরাপদ (= rate limiting)
- গেটের সামনে লাইন অনেক লম্বা হলে → নতুন লোকদের “পরের ট্রেনে আসুন” বলা হয় (= 429 Too Many Requests)
গুরুত্বপূর্ণ ইনসাইট: রেট লিমিট ট্রাফিক কমায় না, ট্রাফিককে predictable করে। আপনার সার্ভার ৫০০০ rps হ্যান্ডেল করতে পারলে rate limit নিশ্চিত করে যে কখনো ৫০০০০ rps ভেতরে ঢুকবে না।
১.২ কেন দরকার — ৬টি আসল কারণ
┌─────────────────────────────────────────────────────────────┐
│ ১. Availability → একজন abuser সবার সার্ভিস নষ্ট করতে পারে না │
│ ২. Cost control → SMS/Mail/3rd-party API বিল নিয়ন্ত্রণ │
│ ৩. Security → brute force, credential stuffing, enumeration │
│ ৪. Fairness → একজন heavy user সব capacity খেয়ে ফেলে না │
│ ৫. Business model → free/pro/enterprise tier আলাদা করা │
│ ৬. Downstream → নিজের DB/queue/partner API-কে বাঁচানো │
└─────────────────────────────────────────────────────────────┘
আপনার প্রজেক্টের কনটেক্সটে:
- কারণ ২ — OTP পাঠানোর API-তে লিমিট না থাকলে একজন অ্যাটাকার স্ক্রিপ্ট চালিয়ে লক্ষ SMS পাঠাতে পারে, এবং বিলটা আসবে আপনার নামে। এজন্যই আপনি
RATE_LIMIT_SPEND_BUDGETS-এsms/mailচ্যানেল আলাদা করেছেন। - কারণ ৩ — forgot-password ফ্লো-তে existence cross-check থাকায় ওটা একটা enumeration oracle; uniform 200 + rate limit দুটোই লাগে।
- কারণ ৬ —
paid_places/paid_geocodeচ্যানেল আসলে Google Places/Geocoding API-র প্রতি রিকোয়েস্টে টাকা কাটে, তাই ওখানে rate নয়, spend নিয়ন্ত্রণ করা লাগে।
১.৩ মৌলিক পরিভাষা
| টার্ম | মানে | উদাহরণ |
|---|---|---|
| Limit | সর্বোচ্চ কতবার | ১০০ |
| Window | কত সময়ে | ১ মিনিট |
| Rate | limit ÷ window | 100/min ≈ 1.67 rps |
| Burst | মুহূর্তের জন্য rate ছাড়িয়ে যাওয়ার অনুমতি | 100/min হলেও একসাথে ২০টা |
| Quota | বড় সময়ের মোট সীমা (সাধারণত বিলিং) | ১০ লক্ষ কল/মাস |
| Cost / Weight | এক রিকোয়েস্ট কত “দামি” | সার্চ = 1, রিপোর্ট = 50 |
| Key / Scope | কাকে গোনা হচ্ছে | ip, user_id, api_key, tenant |
| Throttle | ধীর করা (reject না করে) | প্রতি রিকোয়েস্টে 200ms delay |
| Shed | ওভারলোডে ইচ্ছাকৃতভাবে ফেলে দেওয়া | 503 |
| Backpressure | ধীরগতি উৎসের দিকে ফিরিয়ে দেওয়া সিগন্যাল | consumer lag → producer pause |
| Concurrency limit | একসাথে কয়টা চলবে | সর্বোচ্চ ৫০টা in-flight |
১.৪ কাছাকাছি ধারণাগুলোর পার্থক্য (খুব গুরুত্বপূর্ণ)
| ধারণা | কী নিয়ন্ত্রণ করে | ট্রিগার | রেসপন্স |
|---|---|---|---|
| Rate limiting | সময়প্রতি সংখ্যা | policy অতিক্রম | 429 |
| Concurrency limiting | একসাথে চলমান সংখ্যা | in-flight > N | 429 / queue |
| Throttling | গতি | নরম সীমা | delay, তারপর 429 |
| Load shedding | সার্ভারের স্বাস্থ্য | CPU/latency/queue depth | 503 |
| Backpressure | পাইপলাইন প্রবাহ | downstream ধীর | producer থামে |
| Circuit breaker | ব্যর্থতার হার | error rate > X% | fail fast |
| Quota | মোট ব্যবহার | মাসিক সীমা | 402/403 |
মনে রাখার সূত্র:
- Rate limit বলে — “তুমি বেশি চাচ্ছো” (client-এর দোষ) → 429
- Load shed বলে — “আমি এখন পারছি না” (server-এর অবস্থা) → 503
এই দুটো গুলিয়ে ফেলা সবচেয়ে কমন ভুল। 429 মানে ক্লায়েন্ট নিজের আচরণ বদলাবে; 503 মানে ক্লায়েন্ট পরে আবার চেষ্টা করবে, কিন্তু দোষটা তার নয়।
১.৫ Active Recall — পর্ব ১
- রেট লিমিট কি ট্রাফিক কমায়? না হলে কী করে?
- 429 আর 503-এর সেমান্টিক পার্থক্য কী?
- আপনার OTP ফ্লো-তে “rate” নিয়ন্ত্রণই কি যথেষ্ট, নাকি “spend” আলাদাভাবে নিয়ন্ত্রণ করা লাগে? কেন?
- Quota আর rate limit-এর মধ্যে পার্থক্য এক লাইনে বলুন।
পর্ব ২ — রেট লিমিটারের অ্যানাটমি
যেকোনো রেট লিমিটার তিনটা প্রশ্নের উত্তর দেয়:
┌────────────────────────────────────────────────────┐
│ ১. WHO? → Key / Scope (কাকে গুনছি) │
│ ২. HOW MUCH? → Cost / Weight (এই কলের ওজন কত) │
│ ৩. HOW MANY? → Policy (কত/কত সময়ে + burst) │
└────────────────────────────────────────────────────┘
২.১ Key design — সবচেয়ে বেশি ভুল হয় এখানে
Key ভুল হলে অ্যালগরিদম যত ভালোই হোক, লিমিটার অকেজো।
| Scope | Key উদাহরণ | কখন ভালো | দুর্বলতা |
|---|---|---|---|
global |
rl:otp.send:global |
সর্বোচ্চ খরচ/ক্ষতি ক্যাপ করা | সবাই একসাথে ভোগে |
ip |
rl:login:ip:103.x.x.x |
anonymous endpoint | NAT/CGNAT, proxy, spoof |
net |
rl:login:net:103.x.x.0/24 |
botnet একই সাবনেটে | বড় ISP-তে over-block |
user |
rl:search:user:9421 |
authenticated API | লগইনের আগে নেই |
id (identifier) |
rl:otp.send:id:01712xxxxxx |
OTP-র মতো target-ভিত্তিক | attacker আইডি ঘোরাতে পারে |
api_key |
rl:api:key:ak_live_xx |
B2B API | key শেয়ার হলে |
tenant |
rl:api:org:5512 |
multi-tenant SaaS | tenant-এর ভেতরে unfair |
device |
rl:signup:device:<fp> |
মোবাইল অ্যাপ | fingerprint spoof |
সোনালি নিয়ম: একটা endpoint-এ একাধিক scope একসাথে চালান।
আপনার OTP পাঠানোর জন্য বাস্তব লেয়ারিং:
রিকোয়েস্ট আসল
│
├─▶ per-identifier : ঐ নম্বরে ৩/১০মিনিট ← একই ভিক্টিমকে স্প্যাম আটকায়
├─▶ per-ip : ১০/১০মিনিট ← এক মেশিন থেকে বহু নম্বর আটকায়
├─▶ per-subnet(/24) : ৪০/১০মিনিট ← ছোট botnet আটকায়
├─▶ global : ৫০০/মিনিট ← সর্বনাশের সিলিং
└─▶ spend(sms) : দৈনিক ৫০০০ + rolling ← টাকার সিলিং
সব পাস করলে তবেই SMS যাবে
এটাকে বলে defense in depth — একটা লেয়ার বাইপাস হলে পরেরটা ধরবে।
২.২ Cost / weight — সব রিকোয়েস্ট সমান নয়
একটা GET /health আর একটা POST /report/generate কে সমান গোনা মানে হয় health check ব্লক করা, নয়তো report endpoint খুলে দেওয়া।
cost = 1 → সাধারণ read
cost = 5 → সার্চ (Elasticsearch হিট)
cost = 20 → জিওকোডিং (টাকা লাগে)
cost = 100 → রিপোর্ট এক্সপোর্ট
cost = tokens → LLM কল (ইনপুট+আউটপুট টোকেন)
Token bucket এই মডেলে ন্যাচারালি ফিট করে — শুধু ১টার বদলে cost সংখ্যক টোকেন কেটে নিন।
২.৩ ডিসিশন ফ্লো
রিকোয়েস্ট
│
▼
┌───────────────┐ না ┌──────────────────┐
│ policy আছে? ├───────▶│ default policy │
└───────┬───────┘ └──────────────────┘
│ হ্যাঁ
▼
┌───────────────┐ হ্যাঁ ┌──────────────────┐
│ allowlist? ├───────▶│ ALLOW (bypass) │
└───────┬───────┘ └──────────────────┘
│ না
▼
┌───────────────────────┐
│ key বানাও (scope অনুযায়ী)│
└───────┬───────────────┘
▼
┌───────────────────────┐ ব্যর্থ ┌────────────────────────┐
│ Redis-এ atomic check ├───────▶│ fail-open না fail-closed?│
└───────┬───────────────┘ └────────────────────────┘
│
┌─────┴──────┐
▼ ▼
ALLOW DENY → 429 + Retry-After + হেডার + মেট্রিক
│
▼
budget.reserve() ← টাকা খরচ হয় এমন হলে
│
▼
হ্যান্ডলার চালাও
│
┌──┴───┐
▼ ▼
সফল ব্যর্থ → budget.release() ← টাকা ফেরত
২.৪ Fail-open বনাম Fail-closed
Redis ডাউন হলে কী করবেন?
| Fail-open (allow) | Fail-closed (deny) | |
|---|---|---|
| ঝুঁকি | Redis ডাউনের সময় সীমাহীন abuse | Redis ডাউনে সার্ভিস ডাউন |
| কখন | সাধারণ read API, availability মুখ্য | টাকা/সিকিউরিটি জড়িত |
| উদাহরণ | GET /products |
otp.send, payment.initiate |
আপনার RATE_LIMIT_SPEND_BUDGETS fail-closed — এটা সঠিক সিদ্ধান্ত: Redis না থাকলে SMS পাঠানো বন্ধ থাকুক, কিন্তু অনিয়ন্ত্রিত বিল না আসুক। বিপরীতে সাধারণ read endpoint-এ fail-closed করলে Redis-এর একটা hiccup পুরো সাইট নামিয়ে দেবে।
মাস্টার-লেভেল সূক্ষ্মতা: সেরা উত্তর প্রায়ই “hybrid” — Redis ফেল করলে in-process local limiter-এ (কড়া লিমিট সহ) ফলব্যাক করুন। তাতে availability-ও থাকে, সিলিংও থাকে।
২.৫ Active Recall — পর্ব ২
- শুধু per-IP লিমিট দিলে OTP স্প্যামার কীভাবে বাঁচবে? আর শুধু per-identifier দিলে?
costপ্যারামিটার কোন অ্যালগরিদমে সবচেয়ে সহজে বসে?- আপনার কোন policy-গুলো fail-closed হওয়া উচিত, কোনগুলো fail-open?
পর্ব ৩ — ৬টি কোর অ্যালগরিদম
এই পর্বটাই ইন্টারভিউ ও প্রোডাকশন — দুই জায়গার হৃদয়। প্রতিটার জন্য: কাজের ধরন → ডায়াগ্রাম → আসল হিসাব → সুবিধা/অসুবিধা।
৩.১ Fixed Window Counter
আইডিয়া: সময়কে সমান বালতিতে ভাগ করুন (0–59s, 60–119s…)। প্রতি বালতিতে একটা কাউন্টার। কাউন্টার > limit হলে reject।
limit = 100 / minute
12:00:00 ──────────────── 12:01:00 ──────────────── 12:02:00
│ window A │ window B │
│ counter = 0→100 │ counter = 0→100 │
└─────────────────────────┴─────────────────────────┘
রিসেট এখানে
Redis-এ:
INCR rl:api:user:42:1757500800
EXPIRE rl:api:user:42:1757500800 60 (শুধু প্রথমবার)
নিউমেরিক উদাহরণ (এবং ফাঁদ):
limit = 100/min ধরুন। অ্যাটাকার এভাবে করল:
| সময় | রিকোয়েস্ট | উইন্ডো | কাউন্টার | ফল |
|---|---|---|---|---|
| 12:00:59.0 | 100 | A | 0 → 100 | সব পাস |
| 12:01:00.0 | 100 | B (নতুন) | 0 → 100 | সব পাস |
১ সেকেন্ডের ভেতরে ২০০টা রিকোয়েস্ট পাস — অথচ লিমিট বলেছিল ১০০/মিনিট। একে বলে boundary burst problem; সবচেয়ে খারাপ ক্ষেত্রে আপনি 2× limit পেতে পারেন।
সুবিধা: অসম্ভব সহজ, O(1) মেমরি (কী প্রতি ১টা ইন্টিজার), সবচেয়ে দ্রুত। অসুবিধা: boundary burst; উইন্ডো রিসেটে thundering herd (সবাই 12:01:00-এ ঝাঁপায়)।
কখন ব্যবহার করবেন: যখন approximate হলেই চলে এবং লিমিটটা “মোটামুটি” — যেমন api.default টাইপ ব্যাকস্টপ পলিসি।
৩.২ Sliding Window Log
আইডিয়া: প্রতিটা রিকোয়েস্টের টাইমস্ট্যাম্প জমা রাখুন। চেক করার সময় পুরনোগুলো ফেলে দিয়ে গুনুন।
এখন = 12:01:30, window = 60s → বৈধ সীমা: 12:00:30 – 12:01:30
log: [12:00:10] [12:00:45] [12:01:02] [12:01:29]
✗ বাদ ✓ ✓ ✓
গণনা = 3
Redis (Sorted Set) দিয়ে:
ZREMRANGEBYSCORE key 0 (now-60000)
ZCARD key
ZADD key now uuid ← পাস করলে
PEXPIRE key 60000
নিউমেরিক উদাহরণ — এটাই সবচেয়ে নিখুঁত:
limit 100/min, ৩.১-এর সেই অ্যাটাক আবার চালান:
| সময় | রিকোয়েস্ট | লগে গণনা | ফল |
|---|---|---|---|
| 12:00:59 | 100 | 0 → 100 | পাস |
| 12:01:00 | 100 | 100 (আগেরগুলো এখনো ৬০s-এর ভেতরে) | সব reject |
| 12:01:59 | 1 | 100 | reject |
| 12:02:00 | 1 | 0 (আগেরগুলো এক্সপায়ার) | পাস |
boundary burst সম্পূর্ণ নেই।
মেমরির হিসাব (এখানেই সমস্যা):
ধরুন ১০ লক্ষ অ্যাক্টিভ ইউজার, প্রত্যেকে সর্বোচ্চ ১০০ এন্ট্রি। Redis sorted set-এ প্রতি এন্ট্রি ≈ ৭০ বাইট (member + score + skiplist overhead)।
1,000,000 × 100 × 70 বাইট = 7,000,000,000 বাইট ≈ 7 GB
শুধু রেট লিমিট করতে ৭ GB RAM। এজন্যই sliding window log কেবল অল্প-ভলিউম, উচ্চ-মূল্যের endpoint-এ ব্যবহার হয়।
সুবিধা: পুরোপুরি নিখুঁত, boundary ইস্যু নেই। অসুবিধা: O(N) মেমরি প্রতি কী; ZREMRANGEBYSCORE ব্যয়বহুল হতে পারে।
প্রোডাকশন ইনসাইট: আপনার RATE_LIMIT_SPEND_BUDGETS-এ “true sliding-window rolling cap (Redis sorted set)” ঠিক এই অ্যালগরিদম — এবং জায়গামতো বসেছে। SMS-এর মতো টাকার সিলিং-এ approximate হওয়া চলে না, আর ভলিউম দিনে কয়েক হাজার, তাই মেমরিও সমস্যা নয়।
৩.৩ Sliding Window Counter (Weighted / Approximate)
আইডিয়া: Fixed window-এর সস্তা মেমরি + sliding-এর মসৃণতা। আগের ও বর্তমান উইন্ডোর কাউন্টার রেখে ওজন করে যোগ করুন।
সূত্র:
count = আগের_উইন্ডোর_কাউন্ট × (আগের উইন্ডোর যতটুকু এখনো ওভারল্যাপ করছে)
+ বর্তমান_উইন্ডোর_কাউন্ট
window A (12:00) window B (12:01)
├────────────────────────┼────────────────────────┤
│ count = 90 │ count = 20 │
└────────────────────────┴────────────────────────┘
┌──── এখন 12:01:15
◀───── ৬০ সেকেন্ডের স্লাইডিং জানালা ─────▶
(A-এর শেষ ৭৫%) + (B-এর প্রথম ২৫%)
আসল হিসাব:
limit = 100/min, এখন = 12:01:15 (বর্তমান উইন্ডোর ২৫% পার), prev = 90, curr = 20
overlap = 1 − 0.25 = 0.75
count = 90 × 0.75 + 20
= 67.5 + 20
= 87.5 → 87
87 < 100 → ALLOW ✅
আরেকটু পরে, 12:01:30 (৫০% পার), curr বেড়ে ৪০ হয়েছে:
count = 90 × 0.50 + 40 = 45 + 40 = 85 → ALLOW
কিন্তু 12:01:05 (৮.৩% পার) এ curr = 30 হলে:
count = 90 × 0.9167 + 30 = 82.5 + 30 = 112.5 → 112
112 > 100 → DENY ❌
লক্ষ্য করুন — fixed window এখানে ALLOW বলত (নতুন উইন্ডোতে মাত্র ৩০)। sliding counter সঠিকভাবে ধরল।
নির্ভুলতা: এটা ধরে নেয় আগের উইন্ডোতে ট্রাফিক সমানভাবে ছড়ানো ছিল। বাস্তবে Cloudflare-এর প্রকাশিত ডেটায় এই এস্টিমেটের ভুল ১% এরও কম।
মেমরি: প্রতি কী মাত্র ২টা ইন্টিজার। ১০ লক্ষ ইউজার ≈ কয়েকশো MB, ৭ GB নয়।
কখন: এটাই বেশিরভাগ HTTP API-র ডিফল্ট পছন্দ হওয়া উচিত।
৩.৪ Token Bucket ⭐
আইডিয়া: একটা বালতিতে টোকেন জমে ধ্রুব হারে। রিকোয়েস্ট এলে টোকেন খরচ হয়। টোকেন না থাকলে reject।
refill: 5 টোকেন/সেকেন্ড
│
▼
┌──────────────────┐
│ ●●●●●●●●●● │ capacity = 10
│ বর্তমান = 7 │ (এর বেশি জমবে না)
└────────┬─────────┘
│ প্রতি রিকোয়েস্টে −cost
▼
টোকেন ≥ cost ?
├─ হ্যাঁ → ALLOW, টোকেন কাটো
└─ না → DENY
দুইটা প্যারামিটার, দুইটা আলাদা অর্থ:
rate(refill) = টেকসই গড় হার (sustained)capacity(bucket size) = কতটা burst সহ্য করবে
এই দুটো আলাদা করতে পারাই token bucket-এর মূল শক্তি।
নিউমেরিক উদাহরণ:
capacity = 10, refill = 5/s। t=0 তে বালতি ভর্তি (10)।
| সময় | ঘটনা | রিফিল | আগে | পরে | ফল |
|---|---|---|---|---|---|
| t=0.0 | ১২টা রিকোয়েস্ট একসাথে | 0 | 10 | 0 | ১০টা পাস, ২টা DENY |
| t=0.4 | ১টা রিকোয়েস্ট | 0.4×5 = 2 | 2 | 1 | ALLOW |
| t=1.0 | ৪টা রিকোয়েস্ট | 0.6×5 = 3 | 4 | 0 | ৪টাই ALLOW |
| t=1.1 | ১টা রিকোয়েস্ট | 0.1×5 = 0.5 | 0.5 | — | DENY (0.5 < 1) |
| t=1.3 | ১টা রিকোয়েস্ট | 0.2×5 = 1.0 | 1.5 | 0.5 | ALLOW |
Retry-After হিসাব: t=1.1 এ ঘাটতি = 1 − 0.5 = 0.5 টোকেন → 0.5 ÷ 5 = 0.1 সেকেন্ড।
কী স্টোর করতে হয়: মাত্র ২টা ফিল্ড — tokens এবং last_refill_ts। টাইমার লাগে না; চেক করার সময় lazy refill করা হয়:
elapsed = now − last_ts
tokens = min(capacity, tokens + elapsed × rate)
সুবিধা: burst-বান্ধব, মেমরি O(1), cost/weight সরাসরি সাপোর্ট করে, Retry-After নিখুঁতভাবে বের হয়।
অসুবিধা: টিউনিং দুটো নব লাগে; ভুল capacity দিলে অপ্রত্যাশিত burst।
কখন: ডিফল্ট পছন্দ যখন burst allow করা দরকার — যা বাস্তবে প্রায় সব ইউজার-ফেসিং API (মানুষ ধ্রুব হারে ক্লিক করে না)। AWS, Stripe, GitHub — সবাই এটাই ব্যবহার করে।
৩.৫ Leaky Bucket
দুটো ভিন্ন জিনিস একই নামে চলে — গুলিয়ে ফেলবেন না।
(ক) Leaky bucket as a queue (traffic shaping):
রিকোয়েস্ট ─┐
রিকোয়েস্ট ─┼──▶ ┌──────────┐
রিকোয়েস্ট ─┘ │ ▓▓▓▓ │ queue (size = 10)
└────┬─────┘
│ ধ্রুব হারে বের হয় (5/s)
▼
হ্যান্ডলার
কিউ ভর্তি হলে → নতুন রিকোয়েস্ট DROP
আউটপুট সম্পূর্ণ মসৃণ — সবসময় ঠিক 5/s, একটুও burst নয়। রিকোয়েস্ট reject না হয়ে অপেক্ষা করে।
(খ) Leaky bucket as a meter: গাণিতিকভাবে token bucket-এর সমতুল্য (উল্টো দিক থেকে দেখা)।
Token bucket বনাম Leaky bucket (queue):
| Token Bucket | Leaky Bucket (queue) | |
|---|---|---|
| Burst | অনুমতি দেয় (bucket ভর্তি থাকলে) | কখনো নয় |
| আউটপুট | অসম | পুরোপুরি সমান |
| অতিরিক্ত রিকোয়েস্ট | সাথে সাথে reject | কিউতে অপেক্ষা, তারপর drop |
| Latency | যোগ করে না | যোগ করে (queue wait) |
| ভালো | ইউজার-ফেসিং API | outbound/partner API, SMS gateway |
প্রোডাকশন ইনসাইট: আপনার SMS প্রোভাইডার যদি বলে “সর্বোচ্চ ১০ TPS” — তখন token bucket দিয়ে burst পাঠালে প্রোভাইডার আপনাকে ব্লক করবে। ওখানে leaky bucket (queue) দরকার: রিকোয়েস্ট কিউতে রেখে ঠিক ১০/সেকেন্ড হারে ছাড়ুন। ইনবাউন্ডে token bucket, আউটবাউন্ডে leaky bucket — এটা খুব কাজের থাম্ব রুল।
৩.৬ GCRA (Generic Cell Rate Algorithm) — মাস্টার লেভেল
টেলিকমের ATM নেটওয়ার্ক থেকে আসা; redis-cell মডিউলের CL.THROTTLE এটাই চালায়। মাত্র একটা ভ্যালু রাখতে হয় — TAT (Theoretical Arrival Time)।
T = emission interval = period ÷ limit ← দুটো রিকোয়েস্টের আদর্শ ব্যবধান
τ = burst tolerance = (burst − 1) × T ← কতটা আগে আসা মাফ
allow যদি: now ≥ TAT − τ
allow হলে: TAT = max(now, TAT) + T
নিউমেরিক উদাহরণ:
limit = 10/minute, burst = 3
T = 60s ÷ 10 = 6 সেকেন্ড
τ = (3 − 1) × 6 = 12 সেকেন্ড
| সময় | TAT (আগে) | শর্ত: now ≥ TAT − 12 | ফল | TAT (পরে) |
|---|---|---|---|---|
| t=0 | 0 | 0 ≥ −12 ✅ | ALLOW | 0 + 6 = 6 |
| t=0 | 6 | 0 ≥ −6 ✅ | ALLOW | 6 + 6 = 12 |
| t=0 | 12 | 0 ≥ 0 ✅ | ALLOW | 12 + 6 = 18 |
| t=0 | 18 | 0 ≥ 6 ❌ | DENY | অপরিবর্তিত |
| t=3 | 18 | 3 ≥ 6 ❌ | DENY | অপরিবর্তিত |
| t=6 | 18 | 6 ≥ 6 ✅ | ALLOW | 18 + 6 = 24 |
ঠিক ৩টা burst পাস করল, তারপর প্রতি ৬ সেকেন্ডে ১টা — একদম নিখুঁত।
Retry-After = TAT − τ − now (উপরের t=0, ৪র্থ রিকোয়েস্টে = 18 − 12 − 0 = 6 সেকেন্ড)।
সুবিধা: কী প্রতি একটামাত্র ভ্যালু, নিখুঁত (approximation নেই), নিখুঁত Retry-After। অসুবিধা: মানসিকভাবে বুঝতে কঠিন, ডিবাগ করা কঠিন (কাউন্ট দেখা যায় না, শুধু একটা টাইমস্ট্যাম্প)।
৩.৭ তুলনামূলক সারণি
| অ্যালগরিদম | মেমরি/কী | নিখুঁততা | Burst | Cost সাপোর্ট | জটিলতা | কখন |
|---|---|---|---|---|---|---|
| Fixed Window | 1 int | ⭐⭐ (2× ফাঁক) | দুর্ঘটনাবশত | সহজ | ⭐ | সস্তা ব্যাকস্টপ |
| Sliding Log | N entries | ⭐⭐⭐⭐⭐ | না | সহজ | ⭐⭐⭐ | টাকা/সিকিউরিটি, কম ভলিউম |
| Sliding Counter | 2 int | ⭐⭐⭐⭐ | না | সহজ | ⭐⭐ | ডিফল্ট HTTP API |
| Token Bucket | 2 field | ⭐⭐⭐⭐ | নিয়ন্ত্রিত ✅ | চমৎকার | ⭐⭐ | ডিফল্ট, burst দরকার হলে |
| Leaky Bucket | queue | ⭐⭐⭐⭐⭐ | কখনো না | মাঝারি | ⭐⭐⭐ | outbound/partner API |
| GCRA | 1 ts | ⭐⭐⭐⭐⭐ | নিয়ন্ত্রিত ✅ | ভালো | ⭐⭐⭐⭐ | বড় স্কেল, redis-cell |
সিদ্ধান্তের শর্টকাট:
burst দরকার? ──হ্যাঁ──▶ Token Bucket (বা GCRA)
│
না
▼
নিখুঁততা critical (টাকা/সিকিউরিটি)? ──হ্যাঁ──▶ Sliding Window Log
│
না
▼
Sliding Window Counter ← ৮০% ক্ষেত্রে এটাই সঠিক উত্তর
৩.৮ Active Recall — পর্ব ৩
- Fixed window-এ কেন সর্বোচ্চ 2× limit পাস হতে পারে — একটা টাইমলাইন এঁকে দেখান।
- limit=100/min, prev=60, curr=45, বর্তমান উইন্ডোর ৩০% পার — sliding counter কী বলবে? (হিসাব করুন)
- capacity=20, refill=2/s। t=0 তে ভর্তি, ২৫টা রিকোয়েস্ট এল। কয়টা পাস? পরের রিকোয়েস্টের Retry-After কত?
- আপনার SMS gateway ১০ TPS বললে token bucket নাকি leaky bucket — কেন?
- GCRA-তে limit=30/min, burst=5 হলে T এবং τ কত?
পর্ব ৪ — ডিস্ট্রিবিউটেড রেট লিমিটিং
৪.১ কেন in-memory লিমিটার ভেঙে পড়ে
Load Balancer
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ App-1 │ │ App-2 │ │ App-3 │
│ limiter │ │ limiter │ │ limiter │
│ 100/min │ │ 100/min │ │ 100/min │
└─────────┘ └─────────┘ └─────────┘
ইউজার পায়: 100 × 3 = 300/min ❌
Kubernetes-এ autoscale করলে সমস্যা আরও খারাপ — pod ৩ থেকে ২০ হলে কার্যকর লিমিট ২০০০/মিনিট হয়ে যায়, অথচ কোথাও কনফিগ বদলাননি।
সমাধান: শেয়ারড স্টেট (Redis) + atomic অপারেশন।
৪.২ Atomicity — INCR + EXPIRE-এর ফাঁদ
নিচের কোডটা দেখতে ঠিক, কিন্তু ভুল:
count = redis.incr(key) # ← এখানে ক্র্যাশ করলে?
if count == 1:
redis.expire(key, 60) # ← EXPIRE কখনো সেট হবে না
INCR সফল হয়ে EXPIRE-এর আগে প্রসেস মারা গেলে কী-টা চিরস্থায়ী হয়ে যায় — ইউজার চিরতরে ব্লকড, এবং Redis-এ মেমরি লিক। এটা বাস্তবে ঘটে।
আরেকটা রেস — check-then-act:
count = redis.get(key) # দুটো প্রসেসই পড়ল 99
if count < 100: # দুটোই পাস করল
redis.incr(key) # ফল: 101
একমাত্র সঠিক সমাধান: Lua স্ক্রিপ্ট। Redis Lua স্ক্রিপ্ট atomically চলে — মাঝখানে অন্য কোনো কমান্ড ঢুকতে পারে না।
৪.৩ প্রোডাকশন Lua — Token Bucket
-- KEYS[1] : bucket key
-- ARGV[1] : capacity
-- ARGV[2] : refill rate (tokens per second)
-- ARGV[3] : now (milliseconds)
-- ARGV[4] : cost (কত টোকেন লাগবে)
-- return : {allowed(0/1), remaining, retry_after_ms}
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1])
local ts = tonumber(bucket[2])
if tokens == nil then
tokens = capacity
ts = now
end
-- lazy refill
local elapsed = math.max(0, now - ts) / 1000.0
tokens = math.min(capacity, tokens + elapsed * rate)
local allowed = 0
local retry_after = 0
if tokens >= cost then
tokens = tokens - cost
allowed = 1
else
retry_after = math.ceil(((cost - tokens) / rate) * 1000)
end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
-- বালতি সম্পূর্ণ ভরতে যত সময়, তার একটু বেশি TTL → idle key নিজে মুছে যায়
redis.call('PEXPIRE', KEYS[1], math.ceil((capacity / rate) * 1000) + 1000)
return {allowed, math.floor(tokens), retry_after}
৪.৪ প্রোডাকশন Lua — Sliding Window Log
-- KEYS[1] : zset key
-- ARGV[1] : limit
-- ARGV[2] : window (ms)
-- ARGV[3] : now (ms)
-- ARGV[4] : unique member id (uuid)
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- ১. উইন্ডোর বাইরের এন্ট্রি মুছে দাও
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)
-- ২. এখন কতগুলো আছে
local count = redis.call('ZCARD', KEYS[1])
if count < limit then
redis.call('ZADD', KEYS[1], now, ARGV[4])
redis.call('PEXPIRE', KEYS[1], window)
return {1, limit - count - 1, 0}
end
-- ৩. reject — সবচেয়ে পুরনোটা কখন expire করবে?
local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
local retry_after = math.ceil((tonumber(oldest[2]) + window) - now)
redis.call('PEXPIRE', KEYS[1], window)
return {0, 0, retry_after}
সূক্ষ্মতা: reject হলে ZADD করবেন না। করলে অ্যাটাকার হাতুড়ি চালিয়ে উইন্ডো চিরকাল ভর্তি রাখতে পারবে — একে বলে self-inflicted permanent ban।
৪.৫ সময় কার — ক্লায়েন্টের না Redis-এর?
now কোথা থেকে আসবে?
| উৎস | সুবিধা | অসুবিধা |
|---|---|---|
| অ্যাপ সার্ভারের ঘড়ি | নেটওয়ার্ক খরচ নেই | সার্ভারগুলোর মধ্যে clock skew (NTP drift কয়েকশো ms) |
redis.call('TIME') |
একটাই ঘড়ি, skew নেই | পুরনো Redis-এ replication জটিলতা |
সুপারিশ: Redis 5+ এ effect-based replication থাকায় TIME নিরাপদ। মাল্টি-রিজিয়ন বা বহু app server থাকলে TIME ব্যবহার করুন — না হলে দুই সার্ভারের ৫০০ms পার্থক্যে বালতি ভুলভাবে রিফিল হবে।
৪.৬ Hot key সমস্যা ও লেটেন্সি
সমস্যা ১ — লেটেন্সি: প্রতি রিকোয়েস্টে ১টা Redis রাউন্ড-ট্রিপ। একই AZ-তে ~0.5ms, ক্রস-AZ ~2ms। ৫০০০ rps এ এটা সরাসরি আপনার p99-তে যোগ হবে।
সমস্যা ২ — hot key: rl:otp.send:global কী-টা প্রতিটা রিকোয়েস্টে হিট হয়। Redis Cluster-এ একটামাত্র shard সব লোড নেবে।
সমাধান — Local + Global হাইব্রিড (token lease):
┌────────────────────────────────────────────────────────┐
│ App instance-এ ছোট local bucket │
│ │
│ Redis থেকে একবারে ১০০ টোকেন "ইজারা" নাও │
│ → পরের ১০০ রিকোয়েস্ট Redis ছাড়াই সার্ভ হবে │
│ → শেষ হলে আবার ইজারা নাও │
│ │
│ Redis কল কমল ১০০× | খরচ: সামান্য over-admission │
└────────────────────────────────────────────────────────┘
হিসাব: ১০ instance, প্রতিটা ১০০ টোকেন ইজারা নেয়। সবচেয়ে খারাপ ক্ষেত্রে অতিরিক্ত পাস = ১০ × ১০০ = ১০০০। limit যদি ১,০০,০০০/মিনিট হয়, ভুল = ১% — গ্রহণযোগ্য। কিন্তু limit যদি ৫/১০মিনিট (OTP) হয়, ভুল = ২০০০০% — সম্পূর্ণ অগ্রহণযোগ্য।
নিয়ম: কড়া/নিরাপত্তা-সংক্রান্ত লিমিটে সবসময় strict Redis; উচ্চ-ভলিউমের ঢিলেঢালা লিমিটে lease।
সমস্যা ৩ — Cluster এ key distribution: কী-তে {} হ্যাশ ট্যাগ সাবধানে ব্যবহার করুন। rl:{user:42}:search লিখলে ঐ ইউজারের সব লিমিট এক shard-এ যাবে (multi-key Lua-র জন্য দরকার), কিন্তু hot user সেই shard-কে গরম করবে।
৪.৭ redis-cell (GCRA বিল্ট-ইন)
CL.THROTTLE user42 15 30 60 1
│ │ │ │ │
│ │ │ │ └─ cost
│ │ └──┴──── 30 রিকোয়েস্ট প্রতি 60 সেকেন্ড
│ └────────── max burst = 15
└──────────────── key
রিটার্ন: [0/1 blocked, limit, remaining, retry_after, reset_after]
একটামাত্র atomic কমান্ড, Lua লাগে না। মডিউল ইনস্টল করা গেলে (self-managed Redis) দারুণ; তবে AWS ElastiCache-এ কাস্টম মডিউল সাপোর্ট করে না — তাই আপনার Mumbai region সেটআপে Lua-ই পথ।
৪.৮ Active Recall — পর্ব ৪
INCRতারপরEXPIRE— ঠিক কোন মুহূর্তে ক্র্যাশ করলে কী-টা অমর হয়ে যায়?- reject হওয়া রিকোয়েস্ট sliding log-এ ZADD করলে কী বিপদ?
- আপনার ১২টা pod চলছে, প্রতিটা ৫০ টোকেন lease নেয় —
otp.sendpolicy-তে এটা কেন বিপজ্জনক? - ElastiCache-এ redis-cell কেন ব্যবহার করতে পারবেন না?
পর্ব ৫ — কোন লেয়ারে রেট লিমিট বসাবেন
৫.১ সম্পূর্ণ স্ট্যাক
┌──────────────────────────────────────────────────────────────┐
│ ১. ক্লায়েন্ট (মোবাইল/ব্রাউজার) │
│ debounce, throttle, ব্যাকঅফ — UX-এর জন্য, নিরাপত্তা নয় │
├──────────────────────────────────────────────────────────────┤
│ ২. CDN / WAF (Cloudflare) │
│ ভলিউমেট্রিক DDoS, bot, per-IP মোটা লিমিট │
│ ✅ সস্তা, origin-এ পৌঁছানোর আগেই বন্ধ │
│ ❌ আপনার business context জানে না │
├──────────────────────────────────────────────────────────────┤
│ ৩. Load Balancer / Ingress (Nginx, ALB) │
│ connection limit, per-IP request rate │
├──────────────────────────────────────────────────────────────┤
│ ৪. API Gateway (Kong, Envoy, APIGW) │
│ per-API-key, tenant tier, quota │
├──────────────────────────────────────────────────────────────┤
│ ৫. অ্যাপ্লিকেশন middleware │
│ per-user, authenticated context │
├──────────────────────────────────────────────────────────────┤
│ ৬. হ্যান্ডলার/ডেকোরেটর ← আপনার @rate_limit এখানে │
│ endpoint-নির্দিষ্ট, business rule সহ │
├──────────────────────────────────────────────────────────────┤
│ ৭. আউটবাউন্ড / ডাউনস্ট্রিম │
│ partner API TPS, DB connection pool, worker concurrency │
└──────────────────────────────────────────────────────────────┘
৫.২ কোন লেয়ার কী ধরে
| হুমকি | সঠিক লেয়ার | কারণ |
|---|---|---|
| ১০ Gbps SYN flood | CDN/WAF | অ্যাপ পর্যন্ত আসাই উচিত নয় |
| এক IP থেকে ১০,০০০ rps | LB / WAF | সস্তা, শুরুতেই কেটে দেয় |
| একটা API key তার tier ছাড়াচ্ছে | Gateway | tier/billing এখানে জানা |
| একজন ইউজার OTP স্প্যাম করছে | অ্যাপ্লিকেশন | identity + business rule দরকার |
| SMS বাজেট শেষ | অ্যাপ্লিকেশন (spend layer) | টাকার হিসাব শুধু অ্যাপ জানে |
| পার্টনার API ৪২৯ দিচ্ছে | আউটবাউন্ড limiter | নিজের কলের হার নিয়ন্ত্রণ |
মূল নীতি: লিমিটটা যত উপরে (edge-এর কাছে) বসানো যায় ততই সস্তা; কিন্তু যত নিচে নামবেন, ততই বেশি context পাবেন। তাই দুই জায়গাতেই বসান।
৫.৩ Nginx (edge লেয়ার)
# ১০MB জোন ≈ ১,৬০,০০০ IPv4 স্টেট ধরে
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone $http_x_api_key zone=perkey:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_req_status 429;
limit_conn_status 429;
location /api/ {
# burst=20: ২০টা পর্যন্ত কিউ হবে
# nodelay : কিউতে বসিয়ে না রেখে সাথে সাথে ছাড়বে (burst allow)
limit_req zone=perip burst=20 nodelay;
limit_req zone=perkey burst=50;
limit_conn conn 10;
proxy_pass http://app;
}
location /api/auth/ {
limit_req zone=perip burst=3; # nodelay নেই → ইচ্ছাকৃত ধীরগতি
proxy_pass http://app;
}
}
nodelay না দিলে কী হয়: rate=10r/s মানে nginx প্রতি ১০০ms-এ ১টা ছাড়বে, বাকিগুলো কিউতে অপেক্ষা করবে — এটা leaky bucket আচরণ। nodelay দিলে burst-গুলো সাথে সাথে যাবে — এটা token bucket আচরণ। লগইন endpoint-এ delay ভালো (brute force ধীর হয়), সাধারণ API-তে খারাপ (p99 latency বাড়ে)।
গুরুত্বপূর্ণ: Nginx Cloudflare-এর পেছনে থাকলে $binary_remote_addr হবে Cloudflare-এর IP — সব ইউজার একই কী শেয়ার করবে! real_ip_header CF-Connecting-IP + set_real_ip_from অবশ্যই কনফিগার করুন। (বিস্তারিত পর্ব ১০-এ।)
৫.৪ Active Recall — পর্ব ৫
- একই লিমিট Cloudflare আর অ্যাপ্লিকেশন — দুই জায়গায় রাখা কি অপচয়? কেন নয়?
- Nginx-এ
burst=20আরburst=20 nodelay— আচরণে পার্থক্য কী? - SMS spend cap কেন WAF-এ বসানো যায় না?
পর্ব ৬ — HTTP কন্ট্রাক্ট: 429, হেডার ও ব্যাকঅফ
৬.১ সঠিক রেসপন্স
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 42
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 42
RateLimit-Policy: 100;w=60
Cache-Control: no-store
{
"error": {
"code": "rate_limit_exceeded",
"message": "অনেক বেশি রিকোয়েস্ট। ৪২ সেকেন্ড পরে আবার চেষ্টা করুন।",
"retry_after": 42,
"scope": "ip",
"policy": "otp.send.phone"
}
}
৬.২ হেডারের ব্যাখ্যা
| হেডার | মানে | নোট |
|---|---|---|
Retry-After |
কত সেকেন্ড (বা HTTP-date) পরে | সবচেয়ে বেশি সাপোর্টেড, সবসময় দিন |
RateLimit-Limit |
উইন্ডোর সীমা | IETF ড্রাফট স্ট্যান্ডার্ড |
RateLimit-Remaining |
আর কয়টা বাকি | সফল রেসপন্সেও দিন |
RateLimit-Reset |
কত সেকেন্ডে রিসেট | |
X-RateLimit-* |
পুরনো de-facto (GitHub/Twitter) | দুটোই দিলে ক্লায়েন্ট সুবিধা পায় |
সফল রেসপন্সেও হেডার দিন — তাহলে ভালো ক্লায়েন্ট 429 খাওয়ার আগেই নিজেকে ধীর করতে পারে। এটা proactive vs reactive-এর পার্থক্য।
সিকিউরিটি ট্রেড-অফ: RateLimit-Remaining অ্যাটাকারকে বলে দেয় ঠিক কতটা জায়গা আছে। লগইন/OTP-র মতো সংবেদনশীল endpoint-এ শুধু Retry-After দিন, বাকি হেডার বাদ।
৬.৩ ক্লায়েন্ট-সাইড: Exponential Backoff + Jitter
সবাই একসাথে ঠিক Retry-After পরে রিট্রাই করলে thundering herd — আবার সবাই 429 খাবে।
import random, time
def call_with_backoff(fn, max_attempts=5, base=1.0, cap=60.0):
for attempt in range(max_attempts):
resp = fn()
if resp.status_code != 429:
return resp
server_hint = float(resp.headers.get("Retry-After", 0))
# exponential: 1, 2, 4, 8, 16 ...
backoff = min(cap, base * (2 ** attempt))
wait = max(server_hint, backoff)
# full jitter — এটাই সবচেয়ে গুরুত্বপূর্ণ লাইন
wait = random.uniform(0, wait)
time.sleep(wait)
raise Exception("rate limited, সব চেষ্টা ব্যর্থ")
কেন full jitter: ১০০০ ক্লায়েন্ট একসাথে 429 খেল।
| কৌশল | ৪ সেকেন্ডে রিট্রাই আসবে |
|---|---|
| jitter নেই | ঠিক t=4s এ ১০০০টা → আবার ধস |
random(0, 4) (full jitter) |
০–৪ সেকেন্ডে ছড়িয়ে, গড়ে ২৫০/সেকেন্ড ✅ |
AWS-এর প্রকাশিত সিমুলেশনেও “full jitter” সবচেয়ে ভালো ফল দেয় — মোট কাজ ও শেষ হওয়ার সময় দুটোই কমে।
৬.৪ Idempotency — অপরিহার্য সঙ্গী
রিট্রাই করলে ডুপ্লিকেট অর্ডার/পেমেন্ট হতে পারে। তাই লেখার (write) API-তে idempotency key:
POST /api/orders
Idempotency-Key: 8f3a1c2e-...
সার্ভার key দেখে আগের রেসপন্স ফেরত দেবে, নতুন অর্ডার বানাবে না। রেট লিমিট + রিট্রাই = ডুপ্লিকেটের ঝুঁকি — এই দুটো সবসময় একসাথে ডিজাইন করবেন।
৬.৫ Active Recall — পর্ব ৬
Retry-AfterআরRateLimit-Reset— কোনটা কখন?- লগইন endpoint-এ
RateLimit-Remainingনা দেওয়ার কারণ কী? - full jitter না দিলে ১০০০ ক্লায়েন্টের ক্ষেত্রে কী ঘটে — সংখ্যায় বলুন।
পর্ব ৭ — রিয়েল ইমপ্লিমেন্টেশন
৭.১ FastAPI — Redis Lua সহ পূর্ণ লিমিটার
# limiter.py
import time
import uuid
from dataclasses import dataclass
from typing import Literal
import redis.asyncio as redis
TOKEN_BUCKET_LUA = """
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cost = tonumber(ARGV[3])
local t = redis.call('TIME')
local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
local b = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(b[1])
local ts = tonumber(b[2])
if tokens == nil then tokens = capacity; ts = now end
local elapsed = math.max(0, now - ts) / 1000.0
tokens = math.min(capacity, tokens + elapsed * rate)
local allowed, retry = 0, 0
if tokens >= cost then
tokens = tokens - cost
allowed = 1
else
retry = math.ceil(((cost - tokens) / rate) * 1000)
end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil((capacity / rate) * 1000) + 1000)
return {allowed, math.floor(tokens), retry}
"""
@dataclass(frozen=True)
class Policy:
limit: int # প্রতি period-এ কতবার
period: int # সেকেন্ড
burst: int | None = None # bucket capacity
scope: Literal["ip", "net", "user", "id", "global"] = "ip"
fail: Literal["open", "closed"] = "open"
@property
def rate(self) -> float:
return self.limit / self.period
@property
def capacity(self) -> int:
return self.burst or self.limit
@dataclass(frozen=True)
class Decision:
allowed: bool
remaining: int
retry_after_ms: int
class RateLimiter:
def __init__(self, r: redis.Redis):
self.r = r
self._script = r.register_script(TOKEN_BUCKET_LUA)
async def check(self, name: str, ident: str, policy: Policy,
cost: int = 1) -> Decision:
key = f"rl:{name}:{policy.scope}:{ident}"
try:
allowed, remaining, retry = await self._script(
keys=[key],
args=[policy.capacity, policy.rate, cost],
)
except redis.RedisError:
if policy.fail == "closed":
return Decision(False, 0, 1000)
return Decision(True, -1, 0) # fail-open
return Decision(bool(allowed), int(remaining), int(retry))
মিডলওয়্যার:
# middleware.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import math
POLICIES = {
"otp.send.phone": Policy(limit=3, period=600, scope="id", fail="closed"),
"otp.send.ip": Policy(limit=10, period=600, scope="ip", fail="closed"),
"otp.verify": Policy(limit=5, period=300, scope="id", fail="closed"),
"search": Policy(limit=60, period=60, burst=20, scope="user"),
"api.default": Policy(limit=600, period=60, burst=100, scope="user"),
}
def client_ip(request: Request) -> str:
# ⚠️ শুধু trusted proxy-র পেছনে থাকলে তবেই XFF বিশ্বাস করুন
xff = request.headers.get("x-forwarded-for", "")
if xff:
return xff.split(",")[-1].strip() # ডানদিক থেকে, বামদিক থেকে নয়
return request.client.host
@app.middleware("http")
async def rate_limit_mw(request: Request, call_next):
policy = POLICIES["api.default"]
ident = str(getattr(request.state, "user_id", None) or client_ip(request))
d = await limiter.check("api.default", ident, policy)
if not d.allowed:
retry_s = max(1, math.ceil(d.retry_after_ms / 1000))
return JSONResponse(
status_code=429,
content={"error": {"code": "rate_limit_exceeded",
"retry_after": retry_s}},
headers={
"Retry-After": str(retry_s),
"RateLimit-Limit": str(policy.limit),
"RateLimit-Remaining": "0",
"RateLimit-Policy": f"{policy.limit};w={policy.period}",
},
)
response = await call_next(request)
response.headers["RateLimit-Limit"] = str(policy.limit)
response.headers["RateLimit-Remaining"] = str(max(0, d.remaining))
return response
৭.২ Django — ডেকোরেটর প্যাটার্ন (আপনার সিস্টেমের কাঠামো)
# ratelimit/decorators.py
import functools
from django.conf import settings
from django.http import JsonResponse
def rate_limit(*policy_names, cost=1, ident=None):
"""
একাধিক policy একসাথে চেক করে — defense in depth.
@rate_limit("otp.send.phone", "otp.send.ip", "otp.send.net")
"""
def decorator(view):
@functools.wraps(view)
def wrapper(request, *args, **kwargs):
for name in policy_names:
policy = settings.RATE_LIMIT_POLICIES[name]
key_val = resolve_identity(request, policy["scope"], ident)
decision = limiter.check(name, key_val, policy, cost)
metrics.incr("rate_limit_decisions_total",
policy=name, scope=policy["scope"],
result="allow" if decision.allowed else "deny")
if not decision.allowed:
retry = max(1, decision.retry_after_ms // 1000)
return JsonResponse(
{"error": {"code": "rate_limit_exceeded",
"retry_after": retry}},
status=429,
headers={"Retry-After": str(retry)},
)
return view(request, *args, **kwargs)
return wrapper
return decorator
settings.py-তে পলিসি (কোডে হার্ডকোড নয় — এটাই ঠিক প্যাটার্ন):
RATE_LIMIT_POLICIES = {
"otp.send.forgot": {"limit": 3, "period": 600, "scope": "id", "fail": "closed"},
"otp.send.email": {"limit": 5, "period": 3600, "scope": "id", "fail": "closed"},
"otp.send.phone": {"limit": 3, "period": 600, "scope": "id", "fail": "closed"},
"otp.verify": {"limit": 5, "period": 300, "scope": "id", "fail": "closed"},
"payment.initiate":{"limit": 5, "period": 300, "scope": "user","fail": "closed"},
"paid.geocode": {"limit": 30, "period": 60, "scope": "user","fail": "closed"},
"api.default": {"limit": 600,"period": 60, "scope": "user","fail": "open"},
}
৭.৩ Spend layer — কেন কাউন্টার যথেষ্ট নয়
এটাই সেই জায়গা যেখানে বেশিরভাগ টিউটোরিয়াল থেমে যায়, কিন্তু বাস্তব সিস্টেম শুরু হয়।
সমস্যা: rate limit বলে “৩টা রিকোয়েস্ট পাস”। কিন্তু SMS পাঠাতে গিয়ে provider timeout দিল। টোকেন কাটা হয়ে গেছে, SMS যায়নি — ইউজার শাস্তি পেল বিনা কারণে। উল্টোদিকে, রিকোয়েস্ট পাস করার পর SMS পাঠাতে গিয়ে ২০ বার রিট্রাই হলে ২০টা SMS-এর বিল এল, অথচ কাউন্টার বলছে ১।
সমাধান — reserve/release (দুই-পর্যায়ের কমিট):
budget = SpendBudget("sms")
# ১. আগে সংরক্ষণ (atomic)
token = budget.reserve(amount=1, ident=phone)
if token is None:
return error("দৈনিক SMS বাজেট শেষ")
try:
provider.send_sms(phone, message) # আসল খরচ এখানে
budget.commit(token) # নিশ্চিত করো
except ProviderError:
budget.release(token) # ফেরত দাও — ইউজারকে শাস্তি নয়
raise
reserve commit / release
│ │
▼ ▼
┌──────────┐ সফল হলে ┌──────────┐
│ RESERVED ├──────────────▶│ COMMITTED│ বাজেট থেকে কাটা গেল
└────┬─────┘ └──────────┘
│ ব্যর্থ / timeout
▼
┌──────────┐
│ RELEASED │ বাজেট ফেরত
└──────────┘
⚠️ release না হলে? → reservation-এ TTL দিন, নিজে expire হবে
দ্বৈত ক্যাপ (আপনার সিস্টেমে যা আছে):
দৈনিক ক্যাপ : 5000 SMS/দিন → fixed window (সহজ, বিলিং-সংগত)
রোলিং ক্যাপ : 500 SMS/ঘণ্টা → sorted set (নিখুঁত, স্পাইক ধরে)
কেন দুটোই?
শুধু দৈনিক থাকলে → রাত ১২:০০ টায় ৫০০০ SMS একসাথে যেতে পারে
শুধু রোলিং থাকলে → দিনে ১২০০০ SMS যেতে পারে (৫০০ × ২৪)
৭.৪ Active Recall — পর্ব ৭
reserve/releaseনা থাকলে provider timeout-এ ইউজার কীভাবে ভুক্তভোগী হয়?- reservation-এ TTL না দিলে কী হবে?
- দৈনিক ও রোলিং — দুটোই কেন লাগে? প্রতিটার একক ব্যর্থতা সংখ্যায় দেখান।
পর্ব ৮ — অ্যাডভান্সড প্যাটার্ন
৮.১ Cost-based / Weighted rate limiting
সব রিকোয়েস্টের দাম সমান নয়। Token bucket-এ শুধু cost বদলান।
COST_TABLE = {
("GET", "/api/health"): 0, # ফ্রি
("GET", "/api/products"): 1,
("GET", "/api/search"): 5, # Elasticsearch
("POST", "/api/geocode"): 20, # টাকা লাগে
("POST", "/api/report/export"):100, # ভারী
}
LLM API — টোকেন-ভিত্তিক দ্বৈত লিমিট:
দুটো আলাদা বালতি একসাথে চলে:
RPM bucket : 500 requests/minute
TPM bucket : 200,000 tokens/minute
প্রি-চেক : estimated_tokens = input_tokens + max_tokens ← আগে reserve
পোস্ট-অ্যাডজাস্ট: আসল খরচ জানার পর অতিরিক্ত টোকেন ফেরত
উদাহরণ: 1000 in + max_tokens 4000 → 5000 reserve
আসলে 800 আউটপুট → 1800 ব্যবহৃত → 3200 ফেরত
এটা ঠিক আপনার budget.reserve/release প্যাটার্নেরই আরেক রূপ।
৮.২ Tiered / Plan-based
TIERS = {
"free": {"limit": 60, "period": 60, "burst": 10, "concurrency": 2},
"pro": {"limit": 1000, "period": 60, "burst": 200, "concurrency": 10},
"enterprise": {"limit": 10000, "period": 60, "burst": 2000, "concurrency": 50},
}
সূক্ষ্মতা: tier আপগ্রেড করলে বালতি সাথে সাথে বড় হবে কি? পুরনো bucket key-তে ছোট capacity লেখা থাকতে পারে। সমাধান — কী-তে tier version এনকোড করুন: rl:api:user:42:v3। tier বদলালে version বাড়ান, নতুন বালতি ভর্তি অবস্থায় শুরু হবে।
৮.৩ Adaptive / Dynamic rate limiting (AIMD)
স্ট্যাটিক লিমিট সবসময় ভুল — হয় খুব কড়া (ক্যাপাসিটি নষ্ট), নয় খুব ঢিলা (ওভারলোড)। সমাধান: সিস্টেমের স্বাস্থ্য দেখে লিমিট নিজে বদলাক।
AIMD (Additive Increase, Multiplicative Decrease) — TCP congestion control-এর মতো:
প্রতি ১০ সেকেন্ডে:
if p99_latency < 200ms এবং error_rate < 1%:
limit = min(max_limit, limit + 10) ← ধীরে বাড়াও
elif p99_latency > 500ms বা error_rate > 5%:
limit = max(min_limit, limit × 0.7) ← দ্রুত কমাও
সিমুলেশন:
| চক্র | p99 | limit (আগে) | কাজ | limit (পরে) |
|---|---|---|---|---|
| 1 | 120ms | 500 | +10 | 510 |
| 2 | 140ms | 510 | +10 | 520 |
| 3 | 610ms | 520 | ×0.7 | 364 |
| 4 | 180ms | 364 | +10 | 374 |
| 5 | 160ms | 374 | +10 | 384 |
লক্ষ্য করুন — বাড়ে ধীরে, কমে দ্রুত। এটা ইচ্ছাকৃত: ওভারলোড থেকে দ্রুত সরে আসা জরুরি, ক্যাপাসিটি ফিরে পাওয়া ধীর হলেও চলে।
limit
│ ╱╲ ╱╲ ╱╲
│ ╱ ╲ ╱ ╲ ╱ ╲ ← করাত-দাঁত প্যাটার্ন
│ ╱ ╲ ╱ ╲ ╱ ╲
│╱ ╲╱ ╲╱
└──────────────────────────────────────▶ সময়
ধীরে বাড়ে হঠাৎ কমে
৮.৪ Concurrency limiting ও Little’s Law
রেট নয়, একসাথে কয়টা চলছে — সেটাই আসল রিসোর্স।
Little's Law: L = λ × W
│ │ └─ গড় লেটেন্সি (সেকেন্ড)
│ └───── আগমন হার (rps)
└───────── গড় concurrency
হিসাব: আপনার API-তে 500 rps আসে, গড় লেটেন্সি 200ms।
L = 500 × 0.2 = 100 টা রিকোয়েস্ট সবসময় in-flight
DB pool-এ যদি ৫০টা কানেকশন থাকে — আপনি ইতিমধ্যেই ২× ওভারসাবস্ক্রাইবড, কিউ জমছে।
এখন ডাউনস্ট্রিম ধীর হয়ে লেটেন্সি 200ms → 2s হলে:
L = 500 × 2 = 1000 in-flight
রেট একই আছে, কিন্তু concurrency ১০ গুণ। rate limiter এটা ধরবেই না — কারণ rps বদলায়নি। এজন্যই concurrency limiter আলাদাভাবে দরকার।
import asyncio
sem = asyncio.Semaphore(50)
async def handler(request):
try:
await asyncio.wait_for(sem.acquire(), timeout=0.05)
except asyncio.TimeoutError:
return JSONResponse({"error": "server busy"}, status_code=503)
try:
return await do_work(request)
finally:
sem.release()
নোট: এটা 503, 429 নয় — কারণ সমস্যাটা সার্ভারের, ক্লায়েন্টের নয় (পর্ব ১.৪ মনে করুন)।
৮.৫ Fairness — Shuffle Sharding
Multi-tenant সিস্টেমে একজন noisy tenant সবাইকে ফেলে দিতে পারে। সাধারণ sharding-এ ৮টা worker pool থাকলে ঐ pool-এর সব tenant ভোগে।
সাধারণ shard: tenant → hash → shard 3
shard 3-এর সব tenant একসাথে ভোগে
Shuffle shard: tenant → ৮টার মধ্যে যেকোনো ২টা shard
tenant A → {1, 5}
tenant B → {1, 7} ← A খারাপ করলে B-র 7 এখনো ভালো
tenant C → {3, 6} ← সম্পূর্ণ অক্ষত
৮টা shard থেকে ২টা বাছার উপায় = C(8,2) = ২৮টা ভিন্ন কম্বিনেশন। মানে একজন খারাপ tenant-এর সাথে পুরোপুরি ওভারল্যাপ করার সম্ভাবনা ১/২৮ ≈ ৩.৬%। ১০০ জন tenant থাকলে গড়ে ৩-৪ জন ক্ষতিগ্রস্ত হবে, ১০০ জন নয়।
৮.৬ Load shedding ও priority
ওভারলোডে সব সমান নয়:
PRIORITY = {
"payment.callback": 0, # কখনো ফেলবেন না
"checkout": 1,
"login": 2,
"search": 3,
"recommendations": 4, # সবার আগে ফেলুন
}
def should_shed(priority: int) -> bool:
load = current_load_factor() # 0.0 – 1.5
if load < 0.8:
return False
threshold = (load - 0.8) * 10 # 0.8→0, 1.3→5
return priority > (5 - threshold)
উদাহরণ: load = 1.1 → threshold = 3 → priority > 2 হলে ফেলে দাও। মানে search ও recommendations বাদ, কিন্তু payment/checkout/login চালু। সাইট “ধীর” হবে, “ডাউন” হবে না।
৮.৭ আউটবাউন্ড রেট লিমিটিং
আপনার সার্ভিস নিজেই যখন ক্লায়েন্ট (SMS gateway, bKash, Google Places):
┌──────────────────────────────────────────────────────┐
│ ১০টা worker pod, সবাই SMS পাঠাতে চায় │
│ প্রোভাইডার লিমিট: ১০ TPS (global) │
│ │
│ ভুল: প্রতি pod-এ ১০ TPS → বাস্তবে ১০০ TPS → ব্লক │
│ ভুল: প্রতি pod-এ ১ TPS → pod বাড়লে/কমলে ভুল হার │
│ সঠিক: Redis-এ শেয়ারড leaky bucket, worker টোকেন নেয় │
└──────────────────────────────────────────────────────┘
worker ──▶ acquire_token("sms_gateway", tps=10)
│
├─ পেল → পাঠাও
└─ পেল না → অপেক্ষা করো (drop নয়, কারণ SMS হারানো যাবে না)
পার্থক্য মনে রাখুন: ইনবাউন্ডে অতিরিক্ত রিকোয়েস্ট reject করা যায় (ক্লায়েন্ট রিট্রাই করবে)। আউটবাউন্ডে সাধারণত queue + delay করতে হয়, কারণ কাজটা ইতিমধ্যে গ্রহণ করা হয়ে গেছে।
৮.৮ Active Recall — পর্ব ৮
- rps একই থাকা সত্ত্বেও concurrency ১০ গুণ হতে পারে কীভাবে? Little’s Law দিয়ে দেখান।
- AIMD-তে বাড়ানো additive আর কমানো multiplicative — কেন উল্টো নয়?
- ৮ shard থেকে ২টা shuffle shard নিলে blast radius কতটা কমে?
- আউটবাউন্ড লিমিটে reject না করে queue করা কেন?
পর্ব ৯ — ১৫টি রিয়েল-ওয়ার্ল্ড ইউজকেস
প্রতিটার জন্য: সমস্যা → হুমকি → key → অ্যালগরিদম → পলিসি → এজ কেস
ইউজকেস ১ — লগইন (Brute force / Credential stuffing)
| হুমকি | পাসওয়ার্ড গেসিং, ফাঁস হওয়া ক্রেডেনশিয়াল দিয়ে বহু অ্যাকাউন্টে চেষ্টা |
| key | তিন স্তর: username, ip, ip_subnet |
| অ্যালগরিদম | Sliding window log (নিখুঁততা দরকার) |
per-username : 5 ব্যর্থ / 15 মিনিট → তারপর ৩০ মিনিট লক
per-ip : 20 ব্যর্থ / 15 মিনিট
per-/24 : 100 ব্যর্থ / 15 মিনিট
global : ব্যর্থতার হার > 30% → সবার জন্য CAPTCHA চালু
কেন তিন স্তর:
- শুধু per-username → অ্যাটাকার ১০,০০০ ইউজারনেমে ৫টা করে চেষ্টা করবে (= credential stuffing), কোনো লিমিট ট্রিগার হবে না
- শুধু per-IP → botnet-এর ১০০০ IP থেকে প্রতিটায় ১৯টা চেষ্টা = ১৯,০০০ চেষ্টা
গুরুত্বপূর্ণ: শুধু ব্যর্থ চেষ্টা গুনুন। সফল লগইনে কাউন্টার রিসেট করুন — না হলে অফিসের NAT-এর পেছনে থাকা সবাই ব্লক হয়ে যাবে।
এজ কেস — DoS by lockout: username দিয়ে লক করলে অ্যাটাকার ইচ্ছা করে ভুল পাসওয়ার্ড দিয়ে আপনার অ্যাকাউন্ট লক করতে পারে। সমাধান: হার্ড লক না দিয়ে ধাপে ধাপে delay + CAPTCHA, এবং পরিচিত ডিভাইস/IP থেকে এলে ছাড় দিন।
ইউজকেস ২ — OTP পাঠানো (আপনার Flow 2: form fill-up)
| হুমকি | SMS বোমা (ভিক্টিমকে স্প্যাম), বিল উড়ানো, নম্বর এনুমারেশন |
| বিশেষত্ব | অ্যানোনিমাস — কোনো authenticated user নেই |
স্তর ১ — identifier (নম্বর/ইমেইল) : ৩ / ১০ মিনিট, ৫ / ২৪ ঘণ্টা
স্তর ২ — cooldown : পরপর দুই OTP-র মাঝে কমপক্ষে ৬০ সেকেন্ড
স্তর ৩ — IP : ১০ / ১০ মিনিট
স্তর ৪ — /24 সাবনেট : ৪০ / ১০ মিনিট
স্তর ৫ — global : ৫০০ / মিনিট
স্তর ৬ — spend(sms) : দৈনিক ৫০০০ + রোলিং ৫০০/ঘণ্টা [fail-closed]
progressive backoff (খুব কার্যকর):
১ম OTP → সাথে সাথে
২য় OTP → ৬০ সেকেন্ড পরে
৩য় OTP → ৩০০ সেকেন্ড পরে
৪র্থ → ২৪ ঘণ্টা ব্লক
সম্পূর্ণ ফ্লো:
POST /otp/send {"phone": "+8801712345678"}
│
▼
ফরম্যাট ভ্যালিডেশন (Redis হিট করার আগেই — সস্তা)
│
▼
normalize! ("01712345678", "+8801712345678", "8801712345678"
→ সব একই কী হতে হবে, না হলে ৩× বাইপাস)
│
▼
৫টা rate limit চেক (উপরের স্তর ১–৫)
│ কোনোটা ফেল → 429 + Retry-After
▼
budget.reserve("sms", 1)
│ ব্যর্থ → 503 "সাময়িকভাবে অনুপলব্ধ"
▼
OTP জেনারেট (crypto-secure, ৬ ডিজিট)
Fernet challenge টোকেন, ttl=300
│
▼
provider.send()
│
┌────┴────┐
▼ ▼
সফল ব্যর্থ → budget.release() → 502
commit
│
▼
সবসময় uniform 200 (নম্বর আছে কি নেই — ফাঁস করবেন না)
এজ কেস — normalization বাইপাস: আপনার Flow 2-এ existence check নেই বলে অ্যাটাকার নম্বরের ফরম্যাট ঘুরিয়ে (+880..., 880..., 0...) তিনটা আলাদা bucket পেতে পারে। rate limit key বানানোর আগেই canonical form-এ নরমালাইজ করা বাধ্যতামূলক। ইমেইলের ক্ষেত্রে: lowercase, এবং Gmail হলে +tag ও ডট সরানোর কথা ভাবুন (r.a.kib+x@gmail.com = rakib@gmail.com)।
ইউজকেস ৩ — OTP যাচাই (verify)
| হুমকি | ৬-ডিজিট কোড ব্রুট-ফোর্স |
গণিত দিয়ে বুঝুন কেন লিমিট অপরিহার্য:
৬ ডিজিট = 1,000,000 সম্ভাবনা
লিমিট না থাকলে, ১০০ rps এ → 1,000,000 ÷ 100 = 10,000 সেকেন্ড ≈ ২.৮ ঘণ্টায় নিশ্চিত ভাঙবে
OTP-র TTL ৩০০ সেকেন্ড হলেও ৩০০ × ১০০ = ৩০,০০০ চেষ্টা = ৩% সফলতার সম্ভাবনা প্রতি OTP-তে
১০০০ ইউজারে চালালে → ৩০ জনের অ্যাকাউন্ট ভাঙবে
লিমিট দিলে:
৫ চেষ্টা / challenge → 5 ÷ 1,000,000 = 0.0005% সম্ভাবনা
per-challenge : ৫ চেষ্টা, তারপর challenge অকার্যকর (নতুন OTP নিতে হবে)
per-ip : ২০ / ১০ মিনিট
অপরিহার্য নিয়ম: ব্যর্থ চেষ্টার সীমা শেষ হলে challenge টোকেন ধ্বংস করুন, শুধু rate limit নয়। না হলে অ্যাটাকার অপেক্ষা করে করে চালিয়ে যাবে। এবং constant-time comparison ব্যবহার করুন (hmac.compare_digest) — timing attack ঠেকাতে।
ইউজকেস ৪ — পাবলিক API (tier-ভিত্তিক)
Free : 60/min, burst 10, 1,000/দিন
Pro : 1,000/min, burst 200, 500,000/মাস
Enterprise : কাস্টম + ডেডিকেটেড quota
দুই স্তর একসাথে: rate (burst নিয়ন্ত্রণ) + quota (বিলিং)। rate = token bucket, quota = মাসিক কাউন্টার।
rate limit শেষ → 429 (কিছুক্ষণ পরে আবার পাবে)
quota শেষ → 402 Payment Required / 403 (আপগ্রেড লাগবে)
দুটোকে একই স্ট্যাটাস দিলে ক্লায়েন্ট অনন্তকাল রিট্রাই করবে — ৪২৯ দেখে সে ভাববে অপেক্ষা করলেই হবে।
ইউজকেস ৫ — সার্চ / অটোকমপ্লিট
| হুমকি | প্রতি কী-স্ট্রোকে ES কল, স্ক্র্যাপিং |
ক্লায়েন্ট : 300ms debounce (৭০% কল এখানেই কমে)
সার্ভার : per-user 60/min, burst 20 (টাইপিং burst স্বাভাবিক)
per-ip 120/min (anonymous সার্চ)
cost : 5 (ES হিট ব্যয়বহুল)
Debounce বনাম Throttle (ক্লায়েন্ট-সাইড):
কী-স্ট্রোক: r a k i b (প্রতিটা ১০০ms ব্যবধানে)
Debounce(300ms): ─────────────────[rakib] ← শেষ থামার পর ১টা কল
Throttle(300ms): [r]──────[kib]────── ← নিয়মিত ব্যবধানে কল
সার্চ সাজেশনে debounce ✅ | স্ক্রল/রিসাইজে throttle ✅
প্রোডাকশন ইনসাইট: আপনার Elasticsearch sync pipeline-এ ES cluster-ই বটলনেক। সার্চ rate limit শুধু abuse ঠেকায় না — এটা আসলে আপনার ES-এর জন্য admission control।
ইউজকেস ৬ — ফাইল আপলোড
সংখ্যা : 10 আপলোড / ঘণ্টা / user
বাইট : 500 MB / দিন / user ← token bucket, cost = ফাইল সাইজ
concurrency: 2 একসাথে / user
আকার : সর্বোচ্চ 50 MB / ফাইল
গুরুত্বপূর্ণ: Content-Length হেডার দেখে আপলোড শুরুর আগেই reject করুন। পুরো ফাইল নিয়ে তারপর 429 দেওয়া মানে ব্যান্ডউইথ ইতিমধ্যেই খরচ হয়ে গেছে — অ্যাটাকার এটাকেই অস্ত্র বানাবে।
ইউজকেস ৭ — পেমেন্ট ইনিশিয়েট
| হুমকি | কার্ড টেস্টিং (চুরি করা কার্ড যাচাই), ডুপ্লিকেট চার্জ |
per-user : 5 / 5 মিনিট [fail-closed]
per-card : 3 ব্যর্থ / ঘণ্টা (hash করা)
per-ip : 10 / ঘণ্টা
ব্যর্থতার হার: একই user-এ >50% ব্যর্থ → ম্যানুয়াল রিভিউ
অবশ্যই idempotency key — না হলে রিট্রাইয়ে ডাবল চার্জ। এটা রেট লিমিটের সাথে অবিচ্ছেদ্য।
ইউজকেস ৮ — ওয়েবহুক (দুই দিকই)
ইনবাউন্ড (SSLCommerz/bKash আপনাকে কল করছে):
⚠️ এখানে সাধারণ rate limit বিপজ্জনক — বৈধ কলব্যাক ফেলে দিলে টাকা হারাবেন
সঠিক পন্থা:
১. IP allowlist (প্রোভাইডারের রেঞ্জ) ← মূল প্রতিরক্ষা
২. HMAC সিগনেচার যাচাই
৩. rate limit শুধু allowlist-এর বাইরের IP-তে, খুব কড়া
৪. allowlisted IP-তে উদার লিমিট (স্পাইক-এ যেন না ফেলে)
আউটবাউন্ড (আপনি কাস্টমারকে কল করছেন):
per-endpoint : 10 TPS (ধীর গ্রাহককে ডুবিয়ে দেবেন না)
রিট্রাই : 1s, 2s, 4s, 8s... সর্বোচ্চ ২৪ ঘণ্টা, jitter সহ
circuit breaker: টানা ১০ ব্যর্থতা → endpoint সাসপেন্ড + ইমেইল
ইউজকেস ৯ — থার্ড-পার্টি API কল (Google Places / Geocoding)
প্রোভাইডার লিমিট : 50 QPS, $5 প্রতি 1000 কল
আপনার লিমিট : 40 QPS (হেডরুম রাখুন), দৈনিক $50 ক্যাপ
স্তর:
per-user : 30 / মিনিট
global : 40 QPS (leaky bucket — burst নয়)
spend : দৈনিক ১০,০০০ কল [fail-closed]
cache : ৭ দিনের ক্যাশ ← আসলে সবচেয়ে বড় সাশ্রয়
হিসাব: ৭০% ক্যাশ হিট রেট মানে ১০,০০০ ইউজার-রিকোয়েস্টে মাত্র ৩,০০০ প্রোভাইডার কল = $১৫ এর বদলে $৫০… মানে ৭০% খরচ কমল। রেট লিমিটের আগে ক্যাশিং ভাবুন — এটা সবচেয়ে বেশি রিটার্ন দেয়।
ইউজকেস ১০ — LLM API প্রক্সি
RPM : 500 requests/min → token bucket
TPM : 200,000 tokens/min → token bucket, cost = আনুমানিক টোকেন
concurrency : 20 (স্ট্রিমিং কানেকশন দীর্ঘ)
খরচ : দৈনিক $100 ক্যাপ per tenant
আগে reserve, পরে আসল খরচ জেনে সমন্বয় — পর্ব ৮.১ দেখুন।
ইউজকেস ১১ — স্ক্র্যাপার / বট প্রতিরোধ
সংকেত (শুধু rate নয়):
- রিকোয়েস্টের ব্যবধানে অস্বাভাবিক নিয়মিততা (মানুষ এত নিখুঁত নয়)
- User-Agent, TLS fingerprint (JA3)
- Referer অনুপস্থিত
- সব রিকোয়েস্ট ঠিক ক্রমানুসারে (id=1,2,3...)
- JS চ্যালেঞ্জ ফেল
প্রতিক্রিয়ার সিঁড়ি:
সন্দেহ কম → নীরব (শুধু লগ)
মাঝারি → CAPTCHA
বেশি → tarpit (ধীরে ধীরে রেসপন্স)
নিশ্চিত → 429/403
Tarpit কেন কার্যকর: 403 দিলে অ্যাটাকার সাথে সাথে জানে, কৌশল বদলায়। ৩০ সেকেন্ড ধরে ধীরে রেসপন্স দিলে তার স্ক্র্যাপারের থ্রেড আটকে থাকে — তার খরচ বাড়ে, আপনার কমে।
ইউজকেস ১২ — Kafka / Queue কনজিউমার থ্রটলিং
আপনার নোটিফিকেশন সিস্টেমের (১০০M+/দিন) কনটেক্সটে:
সমস্যা: কনজিউমার DB-র চেয়ে দ্রুত পড়লে DB ডুবে যাবে
সমাধানের স্তর:
১. max.poll.records ← একবারে কত মেসেজ
২. কনজিউমার-সাইড token bucket ← প্রতি সেকেন্ডে কত প্রসেস
৩. pause()/resume() ← ডাউনস্ট্রিম ধীর হলে partition থামাও
৪. consumer lag মনিটর ← lag বাড়লে অ্যালার্ট
lag কম lag বাড়ছে
│ │
consumer ──────┴──── DB ─────────┴──── pause(partition)
▲ │
└────────── resume() যখন lag কমে ◀────────┘
মূল পার্থক্য: এখানে reject করার উপায় নেই (মেসেজ Kafka-তে আছেই)। তাই এটা backpressure, rate limit নয় — গতি নিয়ন্ত্রণ, প্রত্যাখ্যান নয়।
ইউজকেস ১৩ — ডেটাবেজ সুরক্ষা
কানেকশন pool : সর্বোচ্চ 50 (PgBouncer-এ transaction pooling)
statement_timeout : 5s ← runaway query কাটো
per-tenant query : 100/min ← একজন tenant DB দখল করবে না
ভারী রিপোর্ট : read replica-তে পাঠাও + concurrency 2
Little’s Law মনে করুন: 500 rps × 200ms = 100 concurrent, কিন্তু pool-এ ৫০। ৫০টা অপেক্ষা করছে → লেটেন্সি বাড়ছে → concurrency আরও বাড়ছে → congestion collapse। rate limiter দিয়ে ভেতরে ঢোকা কমানোই এখানে একমাত্র রক্ষা।
ইউজকেস ১৪ — ইমেইল পাঠানো
per-recipient : 5 / ঘণ্টা (স্প্যাম হিসেবে চিহ্নিত হওয়া এড়াতে)
per-user : 50 / দিন
global : প্রোভাইডারের TPS (leaky bucket)
bounce rate : > 5% হলে থামুন — না হলে ডোমেইন reputation নষ্ট
ব্যবসায়িক ইনসাইট: এখানে rate limit শুধু কারিগরি নয় — bounce/complaint rate বেশি হলে SES/SendGrid আপনার অ্যাকাউন্ট সাসপেন্ড করবে। রেট লিমিট এখানে deliverability protection।
ইউজকেস ১৫ — ভোট / লাইক / রিভিউ (স্প্যাম)
per-user per-item : 1 (idempotent — টগল, কাউন্ট নয়)
per-user global : 100 লাইক / ঘণ্টা
per-item : অস্বাভাবিক স্পাইক ডিটেকশন (১ মিনিটে ১০০০ লাইক?)
নতুন অ্যাকাউন্ট : ২৪ ঘণ্টার আগে ভোট দিতে পারবে না
মূল পাঠ: এখানে সবচেয়ে ভালো “রেট লিমিট” আসলে ডেটা মডেল — UNIQUE(user_id, item_id) কনস্ট্রেইন্ট। একই লাইক দুইবার গোনার সুযোগই না রাখলে rate limit অনেক কম দরকার হয়। যেখানে ইডেম্পোটেন্সি দিয়ে সমস্যা সমাধান করা যায়, সেখানে রেট লিমিট দ্বিতীয় পছন্দ।
পর্ব ১০ — বাইপাস ও সিকিউরিটি
১০.১ X-Forwarded-For স্পুফিং — সবচেয়ে কমন ভুল
অ্যাটাকার পাঠায়:
X-Forwarded-For: 1.2.3.4
আপনার কোড লেখে:
ip = request.headers["X-Forwarded-For"].split(",")[0] ❌
ফল: অ্যাটাকার প্রতি রিকোয়েস্টে নতুন IP বসিয়ে দেয় → অসীম রেট লিমিট
XFF কীভাবে জমা হয়:
ক্লায়েন্ট (স্পুফড: "1.2.3.4")
│ XFF: 1.2.3.4
▼
Cloudflare
│ XFF: 1.2.3.4, <আসল ক্লায়েন্ট IP>
▼
আপনার Nginx
│ XFF: 1.2.3.4, <আসল ক্লায়েন্ট IP>, <CF IP>
▼
অ্যাপ
বাম দিক = অ্যাটাকার যা খুশি লিখতে পারে ❌
ডান দিক = আপনার প্রক্সি যোগ করেছে ✅
সঠিক নিয়ম:
TRUSTED_PROXIES = 2 # Cloudflare + Nginx
def client_ip(request):
# Cloudflare-এর পেছনে থাকলে এটাই সবচেয়ে নিরাপদ
cf = request.headers.get("cf-connecting-ip")
if cf and from_cloudflare(request):
return cf
chain = [p.strip() for p in
request.headers.get("x-forwarded-for", "").split(",") if p.strip()]
if len(chain) >= TRUSTED_PROXIES:
return chain[-TRUSTED_PROXIES] # ডান দিক থেকে গুনুন
return request.client.host
টেস্ট: নিজের API-তে curl -H "X-Forwarded-For: 1.2.3.4" দিয়ে ২০০ বার হিট করুন। ব্লক না হলে আপনার লিমিটার অকেজো।
১০.২ IPv6 — /128 নয়, /64 ধরুন
একজন IPv6 ইউজার সাধারণত পায় /64 বা /48 ব্লক
/64 ব্লক = 2^64 = 18,446,744,073,709,551,616 টা ঠিকানা
per-address লিমিট করলে অ্যাটাকার প্রতি রিকোয়েস্টে নতুন ঠিকানা ব্যবহার করবে
→ রেট লিমিট কার্যত অস্তিত্বহীন
import ipaddress
def rl_key_for_ip(ip: str) -> str:
addr = ipaddress.ip_address(ip)
if addr.version == 6:
return str(ipaddress.ip_network(f"{ip}/64", strict=False))
return str(addr)
১০.৩ NAT / CGNAT — উল্টো সমস্যা
বাংলাদেশে অনেক ISP CGNAT ব্যবহার করে — হাজার হাজার গ্রাহক একই পাবলিক IP শেয়ার করে। কড়া per-IP লিমিট দিলে বৈধ ইউজাররা একে অপরকে ব্লক করবে।
সমাধান:
- authenticated ইউজারে per-user লিমিট ব্যবহার করুন, per-IP নয়
- per-IP লিমিট উদার রাখুন, শুধু ব্যাকস্টপ হিসেবে
- 429 হার মনিটর করুন — নির্দিষ্ট ISP-তে হঠাৎ বাড়লে CGNAT সন্দেহ করুন
১০.৪ রেট লিমিট নিজেই একটা তথ্য ফাঁস (Oracle)
"এই ইমেইলে ৩টা OTP পাঠানো হয়ে গেছে" → নিশ্চিত হলো ইমেইলটা রেজিস্টার্ড ❌
সঠিক: রেজিস্টার্ড ও নন-রেজিস্টার্ড — দুই ক্ষেত্রেই একই আচরণ, একই সময়, একই মেসেজ
আপনার forgot-password ফ্লো-তে uniform 200 দেওয়া ঠিক এই কারণেই — কিন্তু খেয়াল রাখুন rate limit রেসপন্সও যেন uniform থাকে। “রেজিস্টার্ড হলে 429, নাহলে 200” — এটা একই ফাঁস, শুধু আলাদা পথে।
টাইমিং: নন-রেজিস্টার্ড ইমেইলে DB লুকআপ ৫ms, রেজিস্টার্ডে SMS পাঠাতে ৮০০ms — এই পার্থক্যও oracle। সমাধান: SMS পাঠানো async করুন, দুই পথেই একই সময়ে রেসপন্স দিন।
১০.৫ অন্যান্য বাইপাস
| বাইপাস | প্রতিকার |
|---|---|
ইমেইল alias (u+1@gmail.com) |
নরমালাইজ করুন |
ফোন ফরম্যাট (+880 / 880 / 0) |
E.164 canonical form |
কেস (Rakib@ vs rakib@) |
lowercase |
URL ভ্যারিয়েন্ট (/api/v1/x vs /api/v1/x/) |
route নাম দিয়ে policy বাঁধুন, path দিয়ে নয় |
HTTP মেথড (POST vs PUT) |
মেথড-নিরপেক্ষ policy কী |
| নতুন অ্যাকাউন্ট খোলা | signup-এ device/IP লিমিট + বয়সভিত্তিক নিয়ম |
| ডিস্ট্রিবিউটেড botnet | global cap + anomaly detection + CAPTCHA |
১০.৬ Active Recall — পর্ব ১০
- XFF চেইনের বাম দিক থেকে IP নিলে অ্যাটাকার ঠিক কী করবে?
- IPv6-এ /64 ব্যবহার না করলে অ্যাটাকারের হাতে কতগুলো “নতুন ইউজার” থাকে?
- rate limit রেসপন্স কীভাবে enumeration oracle হয়ে যায়?
পর্ব ১১ — অ্যান্টি-প্যাটার্ন (যা করবেন না)
| ❌ ভুল | কেন খারাপ | ✅ সঠিক |
|---|---|---|
| শুধু in-memory limiter | pod সংখ্যা × limit | Redis + Lua |
INCR তারপর আলাদা EXPIRE |
অমর কী, মেমরি লিক | এক Lua স্ক্রিপ্ট |
| সবখানে fail-open | Redis ডাউনে অসীম abuse | সংবেদনশীলে fail-closed |
| সবখানে fail-closed | Redis hiccup = সাইট ডাউন | সাধারণ read-এ fail-open |
429-এ Retry-After না দেওয়া |
ক্লায়েন্ট হাতুড়ি চালাবে | সবসময় দিন |
| jitter ছাড়া রিট্রাই | thundering herd | full jitter |
| রেট লিমিট = নিরাপত্তার একমাত্র স্তর | অ্যাটাকার লিমিটের ঠিক নিচে থাকবে | auth + anomaly detection সহ |
| ভুল রিকোয়েস্টও কোটায় গোনা | ৪০০ ভ্যালিডেশন এররে ইউজার ব্লক | ভ্যালিডেশন আগে, লিমিট পরে |
| ক্লায়েন্ট-সাইড লিমিটে ভরসা | বাইপাস করা তুচ্ছ | সার্ভারেই আসল প্রতিরক্ষা |
| ব্যর্থ ও সফল লগইন একসাথে গোনা | সাধারণ ইউজার ব্লক | শুধু ব্যর্থ গুনুন |
| রেট লিমিট মনিটর না করা | নীরবে ইউজার হারাবেন | মেট্রিক + অ্যালার্ট |
| একটাই global policy | সব endpoint সমান নয় | per-endpoint policy |
| policy কোডে হার্ডকোড | বদলাতে ডিপ্লয় লাগে | config/settings-এ |
| 429 আর 503 গুলিয়ে ফেলা | ক্লায়েন্ট ভুল আচরণ করবে | সেমান্টিক আলাদা রাখুন |
সবচেয়ে সূক্ষ্ম অ্যান্টি-প্যাটার্ন: রেট লিমিট বসিয়ে ভাবা কাজ শেষ। বাস্তবে সবচেয়ে বড় ঝুঁকি হলো আপনার লিমিট আসলে কাজ করছে কি না তা কখনো যাচাই না করা। পর্ব ১২ এজন্যই।
পর্ব ১২ — টেস্টিং ও মনিটরিং
১২.১ যে মেট্রিকগুলো লাগবেই
rate_limit_decisions_total{policy, scope, result="allow|deny"} # counter
rate_limit_check_duration_seconds{policy} # histogram
rate_limit_backend_errors_total{backend="redis"} # counter
spend_budget_remaining{channel="sms|mail"} # gauge
spend_budget_exhausted_total{channel} # counter
যে অনুপাতটা দেখবেন:
deny_ratio = deny / (allow + deny)
< 0.1% → ঠিক আছে
0.1 – 1% → স্বাভাবিক (কিছু abuse ধরছে)
> 5% → হয় অ্যাটাক চলছে, নয় আপনার লিমিট খুব কড়া ← তদন্ত করুন
কে ব্লক হচ্ছে তা দেখুন: deny-র বেশিরভাগ যদি একটা key থেকে আসে → অ্যাটাক। যদি হাজার key-তে ছড়ানো থাকে → আপনার লিমিট ভুল, ইউজার ভুল নয়।
১২.২ অ্যালার্ট
- alert: RateLimitDenySpike
expr: |
sum(rate(rate_limit_decisions_total{result="deny"}[5m]))
/ sum(rate(rate_limit_decisions_total[5m])) > 0.05
for: 5m
- alert: SmsBudgetLow
expr: spend_budget_remaining{channel="sms"} < 500
for: 1m
- alert: RateLimitBackendDown
expr: rate(rate_limit_backend_errors_total[1m]) > 0
for: 30s
শেষটা বিশেষ গুরুত্বপূর্ণ — fail-open থাকলে Redis ডাউন হলে সবকিছু “স্বাভাবিক” দেখাবে, অথচ আপনার সব প্রতিরক্ষা নিষ্ক্রিয়।
১২.৩ লোড টেস্ট (k6)
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
burst: { executor: 'constant-arrival-rate',
rate: 200, timeUnit: '1s', duration: '30s',
preAllocatedVUs: 50 },
},
};
export default function () {
const res = http.get('https://api.example.com/api/search?q=test',
{ headers: { 'Authorization': `Bearer ${__ENV.TOKEN}` }});
check(res, {
'allowed or limited': (r) => r.status === 200 || r.status === 429,
'429 has Retry-After': (r) => r.status !== 429 || !!r.headers['Retry-After'],
'never 5xx': (r) => r.status < 500,
});
}
প্রত্যাশিত ফল: limit যদি 60/min হয়, 200 rps × 30s = 6000 রিকোয়েস্টে প্রায় ৩০টা 200 আর বাকি ৫৯৭০টা 429 পাওয়ার কথা। যদি 5xx আসে — আপনার লিমিটার নিজেই বটলনেক।
১২.৪ ইউনিট টেস্টে সময় নিয়ন্ত্রণ করুন
def test_token_bucket_refill(fake_clock):
limiter = TokenBucket(capacity=10, rate=5, clock=fake_clock)
for _ in range(10):
assert limiter.allow() # ১০টা burst
assert not limiter.allow() # ১১তম DENY
fake_clock.advance(1.0) # ১ সেকেন্ড → +৫ টোকেন
for _ in range(5):
assert limiter.allow()
assert not limiter.allow()
time.sleep() দিয়ে টেস্ট লিখবেন না — টেস্ট ধীর ও flaky হবে। ঘড়িটা সবসময় ইনজেক্ট করার মতো করে ডিজাইন করুন।
পর্ব ১৩ — ডিজাইন ডিসিশন চিটশিট
┌─────────────────────────────────────────────────────────────┐
│ প্রশ্ন ১: কী রক্ষা করছি? │
│ টাকা → spend budget + fail-closed + sliding log │
│ নিরাপত্তা → কড়া, নিখুঁত, progressive backoff │
│ ক্যাপাসিটি → token bucket + concurrency limit + shedding │
│ ন্যায্যতা → per-tenant + shuffle sharding │
│ │
│ প্রশ্ন ২: কাকে চিনি? │
│ authenticated → user/tenant key (নির্ভরযোগ্য) │
│ anonymous → ip + net + global (স্তরে স্তরে) │
│ │
│ প্রশ্ন ৩: burst দরকার? │
│ হ্যাঁ → token bucket / GCRA │
│ না → leaky bucket (queue) / sliding counter │
│ │
│ প্রশ্ন ৪: ভলিউম কত? │
│ < 1000/দিন → sliding log (নিখুঁত, সস্তা) │
│ > 1M/দিন → sliding counter / token bucket + lease │
│ │
│ প্রশ্ন ৫: Redis মরলে? │
│ টাকা/সিকিউরিটি → fail-closed │
│ সাধারণ read → fail-open (+ local fallback) │
└─────────────────────────────────────────────────────────────┘
সংখ্যা বাছার ব্যবহারিক নিয়ম:
১. আসল ট্রাফিক মাপুন — p50, p95, p99 per user
২. প্রাথমিক লিমিট = p99 × 3
(উদাহরণ: p99 = 20 req/min → লিমিট 60/min)
৩. প্রথমে "shadow mode"-এ চালান — deny করবেন না, শুধু লগ করুন
৪. এক সপ্তাহ দেখুন কতজন বৈধ ইউজার ধরা পড়ত
৫. ০.১% এর কম হলে চালু করুন
৬. deny_ratio মনিটর করে টিউন করুন
Shadow mode অপরিহার্য। সরাসরি enforce করে বৈধ ইউজার ব্লক করে ফেলার চেয়ে এক সপ্তাহ অপেক্ষা করা অনেক সস্তা।
পর্ব ১৪ — মাস্টারি রোডম্যাপ ও ইন্টারভিউ
১৪.১ চার ধাপের রোডম্যাপ
Level 1 — Foundation (১–২ দিন)
- rate limit vs throttle vs shed vs backpressure পার্থক্য বলতে পারা
- fixed window নিজে লিখে boundary burst ডেমো করা
- 429 + Retry-After সহ সঠিক রেসপন্স বানানো
Level 2 — Core (৩–৫ দিন)
- ৬টা অ্যালগরিদম কাগজে লিখতে পারা
- token bucket ইন-মেমরি ইমপ্লিমেন্ট + ইউনিট টেস্ট (fake clock)
- Redis + Lua দিয়ে ডিস্ট্রিবিউটেড ভার্সন
- exponential backoff + full jitter ক্লায়েন্ট
Level 3 — Advanced (১ সপ্তাহ)
- multi-scope policy (id + ip + net + global) সিস্টেম
- spend/budget reserve-release লেয়ার
- concurrency limiter + load shedding
- k6 দিয়ে লোড টেস্ট, Grafana ড্যাশবোর্ড
Level 4 — Master (চলমান)
- adaptive AIMD লিমিটার
- shuffle sharding দিয়ে tenant isolation
- local lease + global sync হাইব্রিড
- শূন্য ডাউনটাইমে policy migration
- shadow mode → gradual rollout প্রক্রিয়া
১৪.২ ইন্টারভিউ প্রশ্ন (উত্তরের সংকেতসহ)
-
“একটা ডিস্ট্রিবিউটেড রেট লিমিটার ডিজাইন করুন, ১০ লক্ষ QPS.” → hierarchical: edge (মোটা, per-IP) + app (সূক্ষ্ম, per-user) + local lease। Redis Cluster, hot key এড়াতে key sharding, sliding counter (মেমরির জন্য)।
-
“Token bucket আর leaky bucket কখন কোনটা?” → burst চাইলে token; মসৃণ আউটপুট (partner TPS) চাইলে leaky। ইনবাউন্ড/আউটবাউন্ড ফ্রেমিং দিন।
-
“Redis ডাউন হলে?” → policy-ভিত্তিক fail mode + local fallback limiter; কোন endpoint কোনটা তা যুক্তি দিয়ে বলুন।
-
“একজন ইউজার ঠিক লিমিটের নিচে থেকে abuse করছে।” → rate limit যথেষ্ট নয়; behavioral anomaly detection, cost-based limit, concurrency limit, reputation score।
-
“429 নাকি 503?” → ক্লায়েন্টের আচরণ = 429; সার্ভারের অবস্থা = 503। এই পার্থক্য বলতে পারা সিনিয়রিটির সংকেত।
-
“লিমিটের সংখ্যা কীভাবে ঠিক করবেন?” → পরিমাপ → p99 × 3 → shadow mode → পর্যবেক্ষণ → enforce। “অনুমান করব” বলবেন না।
-
“রেট লিমিটার নিজেই বটলনেক হলে?” → প্রতি রিকোয়েস্টে Redis RTT, hot key; lease/batching, pipelining, local cache, async আপডেট।
পর্ব ১৫ — কনসলিডেশন এক্সারসাইজ
খাতা-কলমে করুন, পরে কোডে।
এক্সারসাইজ ১ — গণনা
(ক) limit=200/min, prev উইন্ডো=180, curr=50, বর্তমান উইন্ডোর ৪০% পার।
Sliding window counter কী বলবে? ALLOW না DENY?
(খ) capacity=50, refill=10/s। t=0 তে ভর্তি। t=0 তে ৬০টা রিকোয়েস্ট এল,
t=2 তে আরও ৩০টা। প্রতিটা ধাপে কয়টা পাস? t=2 এ DENY হলে Retry-After কত?
(গ) GCRA-তে limit=20/min, burst=4। T ও τ বের করুন। t=0 তে ৬টা রিকোয়েস্ট
এলে কয়টা পাস করবে এবং ৬ষ্ঠটার Retry-After কত?
(ঘ) আপনার API-তে 800 rps, গড় লেটেন্সি 150ms। কত concurrent রিকোয়েস্ট?
ডাউনস্ট্রিম ধীর হয়ে লেটেন্সি 1.2s হলে কত? DB pool 100 হলে কী ঘটবে?
এক্সারসাইজ ২ — ডিজাইন
আপনার aggame প্রজেক্টের follow-up তালিকা থেকে login/registration এর জন্য একটা সম্পূর্ণ policy লিখুন:
- কয়টা scope? কোন কোনটা?
- প্রতিটার অ্যালগরিদম ও কেন
- fail-open না fail-closed?
- ব্যর্থ বনাম সফল — কীভাবে গুনবেন?
- CGNAT-এর ইউজাররা যেন ভুক্তভোগী না হয়, তার ব্যবস্থা
- সংখ্যাগুলো কীভাবে ঠিক করবেন (কোন ডেটা দেখে)?
এক্সারসাইজ ৩ — ভাঙা সিস্টেম মেরামত
নিচের কোডে ৬টা বাগ আছে। খুঁজে বের করুন:
def check_rate_limit(request, limit=100, window=60):
ip = request.headers.get("X-Forwarded-For", "").split(",")[0]
key = f"rl:{ip}"
count = redis.get(key)
if count and int(count) >= limit:
return JsonResponse({"error": "too many"}, status=503)
redis.incr(key)
redis.expire(key, window)
return None
উত্তর দেখতে খুলুন
- XFF-এর বাম দিক — অ্যাটাকার স্পুফ করতে পারে (১০.১)
- check-then-act রেস —
getওincrআলাদা, দুই প্রসেস একসাথে পাস করবে (৪.২) - প্রতিবার
expire— উইন্ডো কখনো শেষ হয় না, হাতুড়ি চালালে চিরস্থায়ী ব্লক - স্ট্যাটাস 503, হওয়া উচিত 429 (১.৪)
Retry-Afterহেডার নেই (৬.১)- Redis এরর হ্যান্ডলিং নেই — Redis ডাউনে exception, fail mode অনির্ধারিত (২.৪)
বোনাস: IPv6 /64 হ্যান্ডলিং নেই, endpoint-ভিত্তিক scope নেই, মেট্রিক নেই।
এক্সারসাইজ ৪ — বাস্তবায়ন
আপনার সিস্টেমে যোগ করুন:
rate_limit_decisions_totalমেট্রিক (policy, scope, result লেবেলসহ)- Grafana প্যানেল: policy-প্রতি deny_ratio
- একটা নতুন endpoint shadow mode-এ চালু করে ৭ দিন পর্যবেক্ষণ
- k6 স্ক্রিপ্ট যা প্রমাণ করে
otp.sendলিমিট আসলেই কাজ করছে
দ্রুত রেফারেন্স কার্ড
╔══════════════════════════════════════════════════════════════╗
║ অ্যালগরিদম বাছাই ║
║ ডিফল্ট HTTP API → Sliding Window Counter ║
║ burst দরকার → Token Bucket ║
║ টাকা/নিরাপত্তা → Sliding Window Log ║
║ আউটবাউন্ড/partner → Leaky Bucket (queue) ║
║ বিশাল স্কেল → GCRA ║
║ ║
║ স্ট্যাটাস কোড ║
║ 429 → ক্লায়েন্ট বেশি চাচ্ছে (Retry-After দিন) ║
║ 503 → সার্ভার পারছে না ║
║ 402/403 → quota শেষ (আপগ্রেড লাগবে) ║
║ ║
║ সবসময় করবেন ║
║ ✓ Lua/atomic অপারেশন ✓ Retry-After হেডার ║
║ ✓ multi-scope স্তর ✓ key নরমালাইজেশন ║
║ ✓ মেট্রিক + অ্যালার্ট ✓ shadow mode আগে ║
║ ✓ full jitter ব্যাকঅফ ✓ TTL সব কী-তে ║
║ ║
║ কখনো করবেন না ║
║ ✗ XFF-এর বাম দিক ✗ INCR + আলাদা EXPIRE ║
║ ✗ শুধু in-memory ✗ ক্লায়েন্ট-সাইডে ভরসা ║
║ ✗ reject-কেও ZADD করা ✗ মনিটরিং ছাড়া চালু করা ║
╚══════════════════════════════════════════════════════════════╝
পরবর্তী ধাপ
এই ডকুমেন্টের সবচেয়ে বেশি রিটার্ন যেখানে:
- পর্ব ৩ (অ্যালগরিদম) — কাগজে হাতে হিসাব করুন, শুধু পড়বেন না
- পর্ব ৪.২ (atomicity) — আপনার বিদ্যমান কোডে এই বাগ আছে কি না চেক করুন
- পর্ব ১০.১ (XFF) — আজই
curl -H "X-Forwarded-For: 1.2.3.4"দিয়ে টেস্ট করুন - পর্ব ৯-এর ইউজকেস ১ — আপনার login/registration follow-up-এর ব্লুপ্রিন্ট
শুভকামনা। 🚀