এতক্ষণ যা বানানো হলো সেটা Production-এর জন্য কতটা প্রস্তুত — Middleware দিয়ে Rate Limiting বসানো

এতক্ষণ যা বানানো হলো সেটা Production-এর জন্য কতটা প্রস্তুত — Middleware দিয়ে Rate Limiting বসানো

Z
জাহিদ হাসান তন্ময়
৩০ অক্টোবর, ২০২৬
15 মিনিটের পড়া
🇬🇧 Read in English
|
0 views
অডিও ওভারভিউ ও পডকাস্ট
~১ মিনিট কুইক ওভারভিউ
0%

এতক্ষণ যা বানানো হলো সেটা Production-এর জন্য কতটা প্রস্তুত — Middleware দিয়ে Rate Limiting বসানো
এতক্ষণ যা বানানো হলো সেটা Production-এর জন্য কতটা প্রস্তুত — Middleware দিয়ে Rate Limiting বসানো

আমাদের আগের পর্বে আমরা যখন TypeScript দিয়ে পুরো Agent সিস্টেম Type-safe রাখার Practical গাইড নিয়ে কাজ করেছিলাম, তখন আমাদের Zod স্কিমা, টুল আউটপুট এবং React UI-এর প্রতিটি বাউন্ডারি শতভাগ টাইপ-সেফ করে তুলেছিলাম। ১৩তম পর্ব থেকে ২১তম পর্ব পর্যন্ত আমরা যা যা বানিয়েছি তার দিকে একবার তাকালে দেখবেন—আমাদের DevPulse এজেন্ট এখন ৩০০ মিলিসেকেন্ডে রেসপন্স স্ট্রিম করে, প্রয়োজনমতো Claude 3.5 Sonnet ও GPT-4o ব্যবহার করে, লাইভ গিটহাব API থেকে বাগ রিপোর্ট পড়ে আসে এবং একটি চমৎকার Thinking Timeline দেখায়। ফিচারের দিক থেকে এটি দেখতে একদম সম্পূর্ণ মনে হলেও, প্রোডাকশনে ডিপ্লয় করার আগে একটা কঠিন প্রশ্ন সামনে চলে আসে: আপনি যদি আজই এই অ্যাপের লিংক পাবলিকলি শেয়ার করেন এবং কোনো ইউজার বা বট স্ক্রিপ্ট এসে মাত্র ২ মিনিটে ৬০টি জটিল প্রশ্ন পাঠিয়ে দেয়, তখন কী ঘটবে?

আপনার তৈরি করা Next.js AI Agent-কে সত্যিকারের প্রোডাকশন-প্রস্তুত করতে এবং অপ্রত্যাশিত API বিল (Denial-of-Wallet অ্যাটাক) থেকে বাঁচাতে Next.js Edge Middleware (src/middleware.ts) এবং HTTP-ভিত্তিক Redis (@upstash/ratelimit) ব্যবহার করে একটি Dual-Tier Sliding-Window Rate Limiter বসানো বাধ্যতামূলক। সাধারণ ওয়েব অ্যাপের API কলের চেয়ে AI Agent সম্পূর্ণ আলাদা, কারণ এখানে ইউজারের মাত্র ১টি মেসেজের পেছনে এজেন্ট ব্যাকএন্ডে সর্বোচ্চ ৫টি ধাপে (maxSteps: 5) LLM কল এবং এক্সটার্নাল টুল রান করতে পারে। তাই রিকোয়েস্টটি আপনার মূল সার্ভার বা OpenAI/Claude API স্পর্শ করার আগেই নেটওয়ার্ক এজ (Edge) লেয়ারে আটকানো এবং সঠিক 429 Too Many Requests ও Retry-After হেডার পাঠানোই হলো প্রোডাকশন গ্রেড ইঞ্জিনিয়ারিং।

আজকের এই ২২তম পর্বের মাধ্যমে আমরা আমাদের রোডম্যাপের তৃতীয় ধাপে (Phase 3: Production Readiness, SSE & Persistence, পর্ব ২২–২৪) প্রবেশ করছি।

প্রোডাকশন রেডিনেস অডিট: সাধারণ API-এর চেয়ে AI Agent কেন বেশি ঝুঁকিপূর্ণ?

কোড লেখার আগে চলুন আমরা এতক্ষণ যা যা বানিয়েছি তার একটি নিরপেক্ষ Production Audit করে নিই:

  • Type Safety ও Schema Validation: প্রস্তুত (পর্ব ২১-এ Zod ও ToolContractRegistry দিয়ে সম্পন্ন)।
  • Streaming ও Thinking UI: প্রস্তুত (পর্ব ১৬ ও ২০-এ useChat ও টাইমলাইন দিয়ে সম্পন্ন)।
  • External Tool Timeout: প্রস্তুত (পর্ব ১৯-এ AbortSignal.timeout(5000) দিয়ে সম্পন্ন)।
  • Rate Limiting ও Budget Protection: অরক্ষিত! এই মুহূর্তে যে কেউ /api/chat রাউটে আনলিমিটেড রিকোয়েস্ট পাঠাতে পারে।

সাধারণ CRUD ওয়েব অ্যাপে কোনো ইউজার যদি মিনিটে ৫০ বার পেজ রিফ্রেশ করে, তবে আপনার ডেটাবেসের কিছুই যায় আসে না। কিন্তু মাল্টি-স্টেপ AI Agent-এর হিসাবটা ভয়াবহ রকম আলাদা। আমাদের এজেন্ট যেহেতু maxSteps: 5 নিয়ে চলে, তাই ইউজারের মাত্র ১টি ক্লিকে ব্যাকএন্ডে যা ঘটে:

১. ধাপ ১: সিস্টেম প্রম্পট + ১০ টার্নের চ্যাট হিস্ট্রি + ৪টি টুলের স্কিমা নিয়ে Claude 3.5 Sonnet বা GPT-4o কল হয় (~১,৫০০ ইনপুট টোকেন)। ২. টুল কল: ডকুমেন্টেশন সার্চ এবং লাইভ গিটহাব API কল হয়। ৩. ধাপ ২: টুলের পাঠানো JSON ফলাফল আবার স্ক্র্যাচপ্যাডে যুক্ত হয়ে মডেলের কাছে যায় (~২,২০০ ইনপুট টোকেন + ৫০০ আউটপুট টোকেন)।

অর্থাৎ, ইউজারের মাত্র একটি প্রশ্নেই খরচ হচ্ছে প্রায় ৪,২০০ টোকেন (মডেলভেদে প্রায় ২ থেকে ৫ সেন্ট)! কোনো বট বা কৌতূহলী ভিজিটর যদি একটি সাধারণ লুপ চালিয়ে ১০ মিনিটে ২০০টি রিকোয়েস্ট পাঠায়, তবে চোখের পলকে আপনার ১০ ডলারের API ক্রেডিট পুড়ে ছাই হয়ে যাবে এবং OpenAI/Anthropic আপনার অ্যাকাউন্টকে সাময়িকভাবে ব্লক (Tier Rate Limit) করে দেবে—যার ফলে আপনার আসল ইউজাররাও আর অ্যাপ ব্যবহার করতে পারবেন না।

এআই কোরের সামনে দুই স্তরের এজ মিডলওয়্যার গেট দিয়ে নিয়ন্ত্রিত রিকোয়েস্ট প্রবেশ এবং অতিরিক্ত বার্স্ট ট্রাফিক ব্লক করার থ্রিডি ভিজ্যুয়ালাইজেশন
এআই কোরের সামনে দুই স্তরের এজ মিডলওয়্যার গেট দিয়ে নিয়ন্ত্রিত রিকোয়েস্ট প্রবেশ এবং অতিরিক্ত বার্স্ট ট্রাফিক ব্লক করার থ্রিডি ভিজ্যুয়ালাইজেশন

Next.js Edge Middleware কীভাবে এজেন্টকে সুরক্ষা দেয়?

Next.js-এর src/middleware.ts ফাইলটি রান হয় সরাসরি নেটওয়ার্ক এজ (Edge Runtime) লেয়ারে—অর্থাৎ রিকোয়েস্টটি আপনার /api/chat রাউটে ঢোকার বা LLM SDK লোড করার আগেই Middleware সেটিকে পরীক্ষা করে। যদি দেখা যায় ইউজার তার লিমিট পার করে ফেলেছেন, তবে Middleware সেখানেই রিকোয়েস্টটি ব্লক করে দেয়। এতে আপনার এক চুলও LLM টোকেন খরচ হয় না!

নিচের ফ্লোচার্টে দেখুন আমাদের টু-টিয়ার (Two-Tier) রেট লিমিটার কীভাবে কাজ করে:

📊 Architecture DiagramArchitecture Flow
Interactive diagram rendering...
flowchart TD
    Client([ইউজার / ব্রাউজার ক্লায়েন্ট]) -->|POST /api/chat| Edge[Next.js Edge Middleware - middleware.ts]
    Edge --> Extract[ক্লায়েন্ট IP ও সেশন আইডি নির্ণয়]
    Extract --> BurstCheck{Tier 1: Sliding Window Burst
সর্বোচ্চ ৫টি প্রশ্ন / ৬০ সেকেন্ড}
    
    BurstCheck -->|লিমিট পার হলে| Reject429[HTTP 429 JSON রিটার্ন
+ Retry-After সেকেন্ড]
    BurstCheck -->|পাস করলে| DailyCheck{Tier 2: Daily Quota Guard
সর্বোচ্চ ৪০টি প্রশ্ন / ২৪ ঘণ্টা}
    
    DailyCheck -->|লিমিট পার হলে| Reject429
    DailyCheck -->|পাস করলে| Route[/api/chat রাউটে ফরওয়ার্ড
+ X-RateLimit-Remaining হেডার]
    Route --> LLM[মাল্টি-স্টেপ এজেন্ট লুপ রান
OpenAI / Claude + Tools]
    Reject429 -->|শূন্য LLM টোকেন খরচ| Client
System architecture specification and node flow: flowchart TD Client([ইউজার / ব্রাউজার ক্লায়েন্ট]) -->|POST /api/chat| Edge[Next.js Edge Middleware - middleware.ts] Edge --> Extract[ক্লায়েন্ট IP ও সেশন আইডি নির্ণয়] Extract --> BurstCheck{Tier 1: Sliding Window Burst সর্বোচ্চ ৫টি প্রশ্ন / ৬০ সেকেন্ড} BurstCheck -->|লিমিট পার হলে| Reject429[HTTP 429 JSON রিটার্ন + Retry-After সেকেন্ড] BurstCheck -->|পাস করলে| DailyCheck{Tier 2: Daily Quota Guard সর্বোচ্চ ৪০টি প্রশ্ন / ২৪ ঘণ্টা} DailyCheck -->|লিমিট পার হলে| Reject429 DailyCheck -->|পাস করলে| Route[/api/chat রাউটে ফরওয়ার্ড + X-RateLimit-Remaining হেডার] Route --> LLM[মাল্টি-স্টেপ এজেন্ট লুপ রান OpenAI / Claude + Tools] Reject429 -->|শূন্য LLM টোকেন খরচ| Client

আর্কিটেকচার ফ্লো সামারি (Architecture Flow Summary): ব্রাউজার থেকে আসা প্রতিটি POST /api/chat রিকোয়েস্ট প্রথমে Next.js Edge Middleware-এ বাধাপ্রাপ্ত হয়, যেখানে ক্লায়েন্টের IP থেকে দুটি স্তরে লিমিট চেক করা হয়: Tier 1 বার্স্ট লিমিট (মিনিটে সর্বোচ্চ ৫টি প্রশ্ন) এবং Tier 2 ডেইলি কোটা (২৪ ঘণ্টায় সর্বোচ্চ ৪০টি প্রশ্ন)। কোনো একটি লিমিট অতিক্রম করলেই Middleware শূন্য LLM খরচে সরাসরি এজ লেয়ার থেকেই HTTP 429 এবং Retry-After কাউন্টডাউন রিটার্ন করে, আর বৈধ রিকোয়েস্টগুলোকে রেট-লিমিট হেডারসহ মূল এজেন্টে পাঠিয়ে দেয়।

ধাপ ১: Edge Runtime-এর উপযোগী Redis প্যাকেজ ইনস্টল করা

Next.js Middleware সাধারণ Node.js সার্ভারে চলে না; এটি চলে হালকা Edge (V8 Isolate) রানটাইমে। তাই এখানে সাধারণ TCP-ভিত্তিক Redis (ioredis) কাজ করে না, আবার প্রোডাকশনে মেমোরির ভেতরে সাধারণ জাভাস্ক্রিপ্ট Map() রাখলেও কাজ হয় না (কারণ সার্ভারলেস ফাংশন প্রতিবার নতুন ইনস্ট্যান্সে রিসেট হয়ে যায়)।

এজন্য আমরা ব্যবহার করব HTTP-ভিত্তিক @upstash/ratelimit এবং @upstash/redis, যা বিশেষভাবে Next.js Edge Middleware-এর জন্যই তৈরি:

bash
npm install @upstash/ratelimit @upstash/redis

এরপর আপনার .env.local ফাইলে Upstash Redis-এর ফ্রি টোকেন দুটি যুক্ত করে নিন:

ini
# .env.local
UPSTASH_REDIS_REST_URL=https://your-instance.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-upstash-rest-token

ধাপ ২: Dual-Tier Sliding Window ইঞ্জিন তৈরি করা (src/lib/agent/rate-limiter.ts)

চলুন এবার তৈরি করি src/lib/agent/rate-limiter.ts। এখানে আমরা দুটি আলাদা লিমিটার বসাব: একটি হলো Burst Limiter (যাতে কেউ ১ মিনিটে ৫টির বেশি প্রশ্ন করতে না পারে), আর অন্যটি Daily Quota Limiter (যাতে একজন ভিজিটর দিনে ৪০টির বেশি প্রশ্ন করে আপনার বাজেট শেষ করতে না পারে)। একই সাথে লোকাল পিসিতে ইন্টারনেট বা Redis ছাড়াই যেন আপনি কোড টেস্ট করতে পারেন, সেজন্য আমরা একটি স্বয়ংক্রিয় লোকাল ফলব্যাকও (Fallback) রাখছি:

typescript
// src/lib/agent/rate-limiter.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

export interface RateLimitDecision {
  allowed: boolean;
  limitType: 'burst-per-minute' | 'daily-quota';
  limit: number;
  remaining: number;
  resetTimestampMs: number;
  retryAfterSeconds: number;
}

const hasUpstashEnv =
  Boolean(process.env.UPSTASH_REDIS_REST_URL) &&
  Boolean(process.env.UPSTASH_REDIS_REST_TOKEN);

const redisClient = hasUpstashEnv ? Redis.fromEnv() : null;

const burstLimiter = redisClient
  ? new Ratelimit({
      redis: redisClient,
      limiter: Ratelimit.slidingWindow(5, '60 s'),
      prefix: 'devpulse:ratelimit:burst',
      analytics: true,
    })
  : null;

const dailyLimiter = redisClient
  ? new Ratelimit({
      redis: redisClient,
      limiter: Ratelimit.slidingWindow(40, '24 h'),
      prefix: 'devpulse:ratelimit:daily',
      analytics: false,
    })
  : null;

const localDevMemoryStore = new Map<
  string,
  { count: number; windowResetMs: number }
>();

function checkLocalDevFallback(identifier: string): RateLimitDecision {
  const now = Date.now();
  const windowMs = 60_000;
  const maxBurst = 5;
  const existing = localDevMemoryStore.get(identifier);

  if (!existing || now > existing.windowResetMs) {
    const resetTimestampMs = now + windowMs;
    localDevMemoryStore.set(identifier, { count: 1, windowResetMs: resetTimestampMs });
    return {
      allowed: true,
      limitType: 'burst-per-minute',
      limit: maxBurst,
      remaining: maxBurst - 1,
      resetTimestampMs,
      retryAfterSeconds: 0,
    };
  }

  if (existing.count >= maxBurst) {
    const retryAfterSeconds = Math.max(
      1,
      Math.ceil((existing.windowResetMs - now) / 1000)
    );
    return {
      allowed: false,
      limitType: 'burst-per-minute',
      limit: maxBurst,
      remaining: 0,
      resetTimestampMs: existing.windowResetMs,
      retryAfterSeconds,
    };
  }

  existing.count += 1;
  return {
    allowed: true,
    limitType: 'burst-per-minute',
    limit: maxBurst,
    remaining: maxBurst - existing.count,
    resetTimestampMs: existing.windowResetMs,
    retryAfterSeconds: 0,
  };
}

export async function evaluateAgentRateLimit(
  identifier: string
): Promise<RateLimitDecision> {
  if (!burstLimiter || !dailyLimiter) {
    return checkLocalDevFallback(identifier);
  }

  const now = Date.now();
  const burstResult = await burstLimiter.limit(identifier);

  if (!burstResult.success) {
    return {
      allowed: false,
      limitType: 'burst-per-minute',
      limit: burstResult.limit,
      remaining: burstResult.remaining,
      resetTimestampMs: burstResult.reset,
      retryAfterSeconds: Math.max(1, Math.ceil((burstResult.reset - now) / 1000)),
    };
  }

  const dailyResult = await dailyLimiter.limit(identifier);

  if (!dailyResult.success) {
    return {
      allowed: false,
      limitType: 'daily-quota',
      limit: dailyResult.limit,
      remaining: dailyResult.remaining,
      resetTimestampMs: dailyResult.reset,
      retryAfterSeconds: Math.max(1, Math.ceil((dailyResult.reset - now) / 1000)),
    };
  }

  return {
    allowed: true,
    limitType: 'burst-per-minute',
    limit: burstResult.limit,
    remaining: burstResult.remaining,
    resetTimestampMs: burstResult.reset,
    retryAfterSeconds: 0,
  };
}

এখানে আমরা সাধারণ Fixed Window ব্যবহার না করে কেন Ratelimit.slidingWindow(5, '60 s') ব্যবহার করলাম? ধরুন Fixed Window প্রতি মিনিটের শুরুতে (12:00:00, 12:01:00) রিসেট হয়। তাহলে একজন চতুর ইউজার 12:00:58 সেকেন্ডে ৫টি রিকোয়েস্ট পাঠিয়ে ঠিক ২ সেকেন্ড পর 12:01:01 সেকেন্ডে আরও ৫টি রিকোয়েস্ট পাঠাতে পারবে—ফলে মাত্র ৩ সেকেন্ডে আপনার এজেন্টকে ১০টি ভারী রিকোয়েস্ট সামলাতে হবে! কিন্তু Sliding Window সবসময় সর্বশেষ ৬০ সেকেন্ডের চলমান গড় হিসাব করে, ফলে এই ফাঁক গলে কোনো বার্স্ট অ্যাটাক ঢুকতে পারে না।

ধাপ ৩: Next.js Edge Middleware তৈরি করা (src/middleware.ts)

এবার আমাদের প্রজেক্টের src/middleware.ts ফাইলটি তৈরি করার পালা। এটি শুধুমাত্র /api/chat এবং /api/agent/:path* রাউটগুলোর POST রিকোয়েস্ট চেক করবে এবং লিমিট অতিক্রম করলে স্ট্যান্ডার্ড Retry-After হেডারসহ 429 JSON রেসপন্স পাঠাবে:

typescript
// src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { evaluateAgentRateLimit } from '@/lib/agent/rate-limiter';

function getClientIdentifier(req: NextRequest): string {
  const forwardedFor = req.headers.get('x-forwarded-for');
  if (forwardedFor) {
    const firstIp = forwardedFor.split(',')[0]?.trim();
    if (firstIp) return `ip:${firstIp}`;
  }

  const realIp = req.headers.get('x-real-ip');
  if (realIp) return `ip:${realIp.trim()}`;

  return 'ip:127.0.0.1-local';
}

export async function middleware(req: NextRequest) {
  if (req.method !== 'POST') {
    return NextResponse.next();
  }

  const identifier = getClientIdentifier(req);
  const decision = await evaluateAgentRateLimit(identifier);

  const rateLimitHeaders = new Headers({
    'X-RateLimit-Limit': String(decision.limit),
    'X-RateLimit-Remaining': String(decision.remaining),
    'X-RateLimit-Reset': String(decision.resetTimestampMs),
  });

  if (!decision.allowed) {
    rateLimitHeaders.set('Retry-After', String(decision.retryAfterSeconds));
    rateLimitHeaders.set('Content-Type', 'application/json');

    const friendlyMessage =
      decision.limitType === 'daily-quota'
        ? `Daily AI agent quota reached (${decision.limit} turns/day). Please try again tomorrow.`
        : `Rate limit exceeded (${decision.limit} agent turns per minute). Please wait ${decision.retryAfterSeconds}s before sending another query.`;

    return new NextResponse(
      JSON.stringify({
        error: friendlyMessage,
        code: 'RATE_LIMIT_EXCEEDED',
        limitType: decision.limitType,
        retryAfterSeconds: decision.retryAfterSeconds,
      }),
      {
        status: 429,
        headers: rateLimitHeaders,
      }
    );
  }

  const response = NextResponse.next();
  rateLimitHeaders.forEach((value, key) => {
    response.headers.set(key, value);
  });

  return response;
}

export const config = {
  matcher: ['/api/chat', '/api/agent/:path*'],
};

ফাইলের একদম নিচে থাকা export const config = { matcher: ['/api/chat', '/api/agent/:path*'] }; লাইনটি অত্যন্ত গুরুত্বপূর্ণ। এটি নিশ্চিত করে যে আপনার ওয়েবসাইটের সাধারণ পেজ ভিজিট, ছবি বা CSS ফাইল লোড হওয়ার সময় যেন রেট লিমিটার কল না হয়—কেবলমাত্র এজেন্টকে প্রশ্ন করার সময়ই এটি কাজ করবে।

ধাপ ৪: Server Action গার্ড এবং React UI-তে Cooldown Timer দেখানো

আমরা ১৩ ও ১৮তম পর্বে যে 'use server' Server Action-গুলো বানিয়েছিলাম, সেগুলোর কী হবে? মনে রাখবেন, Server Action কল হয় সরাসরি পেজের রাউটে, /api/chat-এ নয়। তাই যেকোনো Server Action-এর শুরুতে ১ লাইনে রেট লিমিট চেক করার জন্য src/lib/agent/server-action-guard.ts ফাইলটি তৈরি করে রাখুন:

typescript
// src/lib/agent/server-action-guard.ts
import 'server-only';
import { headers } from 'next/headers';
import { evaluateAgentRateLimit, type RateLimitDecision } from './rate-limiter';

export async function assertServerActionRateLimit(): Promise<RateLimitDecision> {
  const headerStore = await headers();
  const forwarded = headerStore.get('x-forwarded-for');
  const clientIp = forwarded ? forwarded.split(',')[0]?.trim() : '127.0.0.1';
  return evaluateAgentRateLimit(`sa-ip:${clientIp}`);
}

এবার ফ্রন্টএন্ডের পালা। ইউজার যখন মিনিটে ৫টির বেশি প্রশ্ন করে ফেলবেন এবং Middleware থেকে 429 এরর আসবে, তখন শুধু একটা লাল রঙের এরর টেক্সট না দেখিয়ে আমরা src/components/RateLimitCooldownBanner.tsx কম্পোনেন্টের মাধ্যমে একটি লাইভ কাউন্টডাউন টাইমার (Wait 14s...) দেখাব। যতক্ষণ কাউন্টডাউন শেষ না হচ্ছে, ততক্ষণ Retry Turn বাটনটি লক থাকবে:

tsx
// src/components/RateLimitCooldownBanner.tsx
'use client';

import { useEffect, useState } from 'react';

interface RateLimitCooldownBannerProps {
  error: Error | undefined;
  onRetry: () => void;
}

function extractCooldownSeconds(errorMessage: string): number {
  try {
    const parsed = JSON.parse(errorMessage) as { retryAfterSeconds?: number };
    if (typeof parsed.retryAfterSeconds === 'number' && parsed.retryAfterSeconds > 0) {
      return parsed.retryAfterSeconds;
    }
  } catch {
    const match = errorMessage.match(/wait\s+(\d+)s/i);
    if (match?.[1]) return Number(match[1]);
  }
  return 15;
}

export default function RateLimitCooldownBanner({
  error,
  onRetry,
}: RateLimitCooldownBannerProps) {
  const [secondsLeft, setSecondsLeft] = useState<number>(0);

  const isRateLimit =
    Boolean(error) &&
    (error?.message.includes('RATE_LIMIT_EXCEEDED') ||
      error?.message.toLowerCase().includes('rate limit') ||
      error?.message.includes('429'));

  useEffect(() => {
    if (!error || !isRateLimit) {
      setSecondsLeft(0);
      return;
    }

    const initial = extractCooldownSeconds(error.message);
    setSecondsLeft(initial);

    const timer = setInterval(() => {
      setSecondsLeft((prev) => (prev <= 1 ? 0 : prev - 1));
    }, 1000);

    return () => clearInterval(timer);
  }, [error, isRateLimit]);

  if (!error) return null;

  return (
    <div className='p-3.5 rounded-xl bg-amber-950/50 border border-amber-800/80 text-xs text-amber-100 flex items-center justify-between gap-4'>
      <div className='space-y-1'>
        <div className='font-semibold text-amber-300'>
          {isRateLimit
            ? '⏳ Agent Rate Limit Cooldown Active'
            : '⚠ Agent Request Error'}
        </div>
        <p className='text-slate-300'>
          {isRateLimit
            ? secondsLeft > 0
              ? `Protecting API token budget. You can send your next query in ${secondsLeft}s.`
              : 'Cooldown complete! You can now retry your turn.'
            : error.message}
        </p>
      </div>

      <button
        type='button'
        disabled={isRateLimit && secondsLeft > 0}
        onClick={onRetry}
        className='px-3 py-1.5 rounded-lg bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white font-semibold shrink-0'
      >
        {isRateLimit && secondsLeft > 0 ? `Wait ${secondsLeft}s` : 'Retry Turn'}
      </button>
    </div>
  );
}

ডার্ক ফ্রস্টেড গ্লাস চ্যাট ইন্টারফেসে অ্যাম্বার রঙের রেট-লিমিট কুলডাউন ব্যানার এবং কাউন্টডাউন রিংয়ের ভিজ্যুয়াল রূপ
ডার্ক ফ্রস্টেড গ্লাস চ্যাট ইন্টারফেসে অ্যাম্বার রঙের রেট-লিমিট কুলডাউন ব্যানার এবং কাউন্টডাউন রিংয়ের ভিজ্যুয়াল রূপ

আমার নিজের একটি প্রোডাকশন শিক্ষা: Vercel-এ ডিপ্লয় করার পর যখন ইন-মেমোরি Map() রেট লিমিটার পুরোপুরি ব্যর্থ হয়েছিল!

আমি যখন আমার বিভিন্ন ওয়েব অ্যাপ (যেমন Alur-Bosta এবং প্রথম দিকের Next.js এজেন্ট প্রোটোটাইপগুলো) Vercel-এ ডিপ্লয় করছিলাম, তখন শুরুতে বাড়তি ডাটাবেস সেটআপের ঝামেলা এড়ানোর জন্য একটি সাধারণ জাভাস্ক্রিপ্ট const rateMap = new Map() দিয়ে রেট লিমিটার বানিয়েছিলাম। আমার নিজের ল্যাপটপে npm run dev চালিয়ে যখন টেস্ট করলাম, তখন সেটি শতভাগ নিখুঁত কাজ করছিল—মিনিটে ৫ বার ক্লিক করার পর ৬ষ্ঠ বারে সাথে সাথে 429 দিয়ে ব্লক করে দিচ্ছিল।

কিন্তু প্রোডাকশনে ডিপ্লয় করার পর আমি যখন একটি ছোট লোড-টেস্ট স্ক্রিপ্ট দিয়ে একসাথে ২৫টি রিকোয়েস্ট পাঠালাম, তখন সার্ভার লগ খুলে আমার চোখ ছানাবড়া হয়ে গেল! ৫টির পর বাকি ২০টি রিকোয়েস্ট ব্লক হওয়ার কথা থাকলেও প্রায় ১৮টি রিকোয়েস্ট কোনো বাধা ছাড়াই ভেতরে ঢুকে LLM API কল করে বসেছে!

কেন এমন হয়েছিল? কারণ Vercel বা যেকোনো Serverless/Edge প্ল্যাটফর্মে আপনার কোড একটি সিঙ্গেল ডেডিকেটেড সার্ভারে চলে না। যখন একসাথে অনেক রিকোয়েস্ট আসে কিংবা কিছুক্ষণ পর ফাংশন আবার নতুন করে চালু (Cold Start) হয়, তখন প্ল্যাটফর্ম আলাদা আলাদা একাধিক কন্টেইনার বা Isolate তৈরি করে—যার প্রতিটির ভেতরে new Map() সম্পূর্ণ নতুন এবং খালি অবস্থায় শুরু হয়! এক ইনস্ট্যান্স জানেই না যে পাশের ইনস্ট্যান্সে ওই একই IP থেকে ইতিমধ্যে ৪টি রিকোয়েস্ট চলে গেছে। তার ওপর শুরুতে config.matcher ঠিকমতো না দেওয়ায় পেজের সাধারণ ফাইল লোডেই অর্ধেক কাউন্টার পূর্ণ হয়ে যাচ্ছিল।

এরপর যখন আমি স্টেটটিকে ইন-মেমোরি থেকে সরিয়ে HTTP-ভিত্তিক Upstash Redis-এ নিয়ে আসি এবং config.matcher শুধুমাত্র /api/chat-এ সীমাবদ্ধ করি, তখন থেকে যত ইনস্ট্যান্সই চালু হোক না কেন, কেন্দ্রীয় স্লাইডিং উইন্ডো ১ মিলিসেকেন্ডেরও এদিক-সেদিক হতে দেয়নি।

Middleware দিয়ে Rate Limiting বসানোর সময় যে ৪টি ভুল এড়িয়ে চলবেন

পরের পর্বে (পর্ব ২৩) Server-Sent Events (SSE) নিয়ে কাজ করার আগে নিশ্চিত হয়ে নিন আপনার কোডে যেন এই ৪টি ভুল না থাকে:

১. প্রোডাকশনে সার্ভারলেস মেমোরির Map()-এর ওপর নির্ভর করা

লোকাল ডেভেলপমেন্টের ফলব্যাক হিসেবে Map() ঠিক আছে, কিন্তু প্রোডাকশন Vercel বা Cloudflare Edge-এ কখনোই ইন-মেমোরি ভেরিয়েবল দিয়ে রেট লিমিট করবেন না। সবসময় Redis বা সমমানের কেন্দ্রীয় অ্যাটমিক স্টোর ব্যবহার করবেন।

২. src/middleware.ts-এ config.matcher না দেওয়া

আপনি যদি matcher না দেন, তবে ইউজার শুধু হোমপেজ ওপেন করলেই ব্রাউজার ব্যাকগ্রাউন্ডে ছবি, ফন্ট ও JS ফাইলের জন্য ১০-১৫টি রিকোয়েস্ট পাঠাবে এবং ইউজার কোনো প্রশ্ন লেখার আগেই সে 429 Rate Limit এরর খেয়ে বসে থাকবে! তাই সবসময় শুধুমাত্র এজেন্টের API রাউটগুলোকেই matcher-এ রাখবেন।

৩. 429 এরর আসার সাথে সাথেই আনলকড Retry বাটন দেখানো

ইউজার যখন রেট লিমিটে আটকে যান, তখন সেখানে সাথে সাথে ক্লিক করার মতো Retry বাটন থাকলে তিনি উত্তেজিত হয়ে পরপর আরও ৫ বার ক্লিক করবেন। তাই আমাদের RateLimitCooldownBanner কম্পোনেন্টের মতো retryAfterSeconds পার্স করে কাউন্টডাউন শেষ না হওয়া পর্যন্ত বাটনটি disabled রাখবেন।

৪. Server Action-কে অরক্ষিত রেখে দেওয়া

মনে রাখবেন, /api/chat পাথ ম্যাচিং আপনার 'use server' Server Action-কে ঢাকতে পারে না, কারণ Server Action কল হয় পেজের নিজস্ব রাউটে। তাই যেকোনো এজেন্ট Server Action-এর ভেতরে আমাদের assertServerActionRateLimit() ফাংশনটি অবশ্যই কল করবেন।

প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী (FAQ)

একটি প্রোডাকশন AI Agent-এর জন্য মিনিটে বা দিনে কতগুলো রিকোয়েস্টের লিমিট দেওয়া আদর্শ?

একটি মাল্টি-স্টেপ এজেন্টের ক্ষেত্রে (যেখানে প্রতিটি প্রশ্নে ৩-৫টি টুল ও LLM কল হতে পারে) সাধারণ ইউজারদের জন্য প্রতি মিনিটে ৫টি প্রশ্ন (Burst Limit) এবং প্রতি ২৪ ঘণ্টায় ৩০ থেকে ৫০টি প্রশ্ন (Daily Quota) সবচেয়ে বাস্তবসম্মত। কারণ একজন রক্ত-মাংসের মানুষের পক্ষে এজেন্টের বড় উত্তর পড়ে মিনিটে ৫টির বেশি অর্থবহ প্রশ্ন করা প্রায় অসম্ভব, কিন্তু বট স্ক্রিপ্টগুলো এই লিমিটে সাথে সাথেই আটকে যায়।

Next.js Middleware থেকে Redis চেক করলে কি এজেন্টের রেসপন্স স্লো হয়ে যাবে?

না। Upstash Redis এজ রানটাইম থেকে HTTP/REST প্রোটোকলে কাজ করে এবং একই রিজিয়নে থাকলে মাত্র ৮ থেকে ২০ মিলিসেকেন্ডের মধ্যে স্লাইডিং উইন্ডো চেক সম্পন্ন করে। ৩০০-৪০০ মিলিসেকেন্ডের LLM কলের তুলনায় এই সামান্য সময়টুকু ইউজার টেরই পান না, অথচ এটি আপনার শত শত ডলারের API বিল বাঁচিয়ে দেয়।

রেট লিমিটিং কি IP অ্যাড্রেস দিয়ে করা উচিত নাকি ইউজারের userId দিয়ে?

সবচেয়ে ভালো পদ্ধতি হলো দুটিই মিলিয়ে ব্যবহার করা: ইউজার যদি লগইন করা থাকেন, তবে তার user:${userId} দিয়ে লিমিট করুন—যাতে একই অফিসের ওয়াই-ফাই (একই পাবলিক IP) ব্যবহার করা ১০ জন ডেভেলপার একে অপরের কোটা ব্লক না করে ফেলেন। আর লগইন ছাড়া পাবলিক ডেমো ভিজিটরদের জন্য x-forwarded-for হেডার থেকে পাওয়া ip:${clientIp} ব্যবহার করুন।

🟢 ফ্রিল্যান্স ও কনট্রাক্ট প্রজেক্টের জন্য উন্মুক্ত

কাস্টম ওয়েব অ্যাপ বা এআই অটোমেশন বানাতে চান?

আমি স্টার্টআপ ও আধুনিক ব্যবসার জন্য স্কেলেবল ফুল-স্ট্যাক ওয়েব অ্যাপ্লিকেশন (Next.js, React, Node, PostgreSQL) এবং ইন্টেলিজেন্ট এআই এজেন্ট সলিউশন তৈরি করি। আপনার প্রজেক্টের আইডিয়া নিয়ে কথা বলা যাক!

ফুল-স্ট্যাক এমভিপি (MVP) ডেভেলপমেন্ট
অটোনোমাস এআই এজেন্ট ও এলএলএম ইন্টিগ্রেশন
পারফেক্ট এসইও ও সর্বোচ্চ পারফরম্যান্স
ক্লিন কোড ও মডার্ন আর্কিটেকচার

আর্টিকেলটি কি আপনার ভালো লেগেছে?

তন্ময়ের কাজকে সাপোর্ট করতে তালি (Clap) দিয়ে উৎসাহিত করুন!

0 views
Z

জাহিদ হাসান তন্ময়

সফটওয়্যার ডেভেলপার

MERN ফুল-স্ট্যাক ডেভেলপার এবং AI এজেন্ট ডেভেলপার, ঢাকা, বাংলাদেশ। ওয়েব ডেভেলপমেন্ট, রিঅ্যাক্ট, লারাভেল এবং আমার লার্নিং জার্নি নিয়ে লিখছি।

Related Articles

Stay Updated

Subscribe to get insights on full-stack architecture, AI agent engineering, and web development.