MCP रेट लिमिटिंग: MCP सर्वर में रेट लिमिट कैसे जोड़ें

TypeScript में MCP रेट लिमिटिंग के लिए एक व्यावहारिक योजना। token bucket बनाएँ, token या client id से कॉलर पहचानें, HTTP पर 429 और Retry-After लौटाएँ, टूल्स को उनकी लागत से तौलें, Redis से काउंटर स्केल करें, और fake timers से हर लिमिट टेस्ट करें, इससे पहले कि कोई एजेंट खामियाँ ढूँढ ले।

MCP रेट लिमिटिंग: MCP सर्वर में रेट लिमिट कैसे जोड़ें
Cristian Da Conceicao
Picasso IA के संस्थापक

एक AI एजेंट कभी बोर नहीं होता। उसे एक अस्पष्ट काम देकर अपने MCP सर्वर की ओर भेज दें, और वह दस सेकंड में 40 टूल कॉल कर सकता है, हर विफलता को तुरंत दोबारा आज़मा सकता है, और ऐसे समानांतर अनुरोध भेज सकता है जिनकी किसी ने योजना नहीं बनाई थी। यह उत्पादकता के लिए बढ़िया है और आपके इनवॉइस के लिए बुरा। MCP रेट लिमिटिंग वह तरीका है जिससे Model Context Protocol सर्वर इस तरह के दबाव में भी काम का बना रहता है: हर कॉलर को एक निष्पक्ष बजट मिलता है, महँगे टूल सस्ते टूल से ज़्यादा टोकन खर्च करते हैं, और जिसका बजट खत्म हो जाता है उसे यह साफ़ संदेश मिलता है कि कब वापस आना है।

यह लेख दिखाता है कि TypeScript में MCP सर्वर में रेट लिमिट कैसे जोड़ें, एक छोटे token bucket से लेकर उस Redis-आधारित लिमिटर तक जो कई इंस्टेंस में काम करता है। आप देखेंगे कि चेक कहाँ लगाने चाहिए, ऐसी त्रुटियाँ कैसे लौटाएँ जिन पर एजेंट कार्रवाई कर सके, और यह सब बिना असली एक मिनट इंतज़ार किए कैसे टेस्ट करें।

MCP सर्वर को रेट लिमिट की ज़रूरत क्यों होती है

गोल्डन आवर में एक हाईवे टोल प्लाज़ा का ऊपर से दृश्य, जिसमें कारें व्यवस्थित लेन में कतार में खड़ी हैं

रेट लिमिट क्षमता के बारे में एक वादा है: यह कॉलर इतना इस्तेमाल कर सकता है, प्रति समय-इकाई, और उससे ज़्यादा नहीं। Model Context Protocol specification के tools के लिए security notes सर्वरों की ज़रूरतों में tool invocations पर रेट लिमिटिंग को इनपुट वैलिडेशन और एक्सेस कंट्रोल के ठीक बगल में रखते हैं। आधिकारिक SDK ट्रांसपोर्ट और स्कीमा संभालते हैं, पर लिमिटर आपके लिए छोड़ देते हैं, इसलिए हर सर्वर लेखक को एक लिखना ही पड़ता है।

एजेंट थके बिना बार-बार दोबारा कोशिश करते हैं

किसी बटन को क्लिक करने वाला इंसान धीमा और आसानी से अनुमान लगाने योग्य होता है। एजेंट लूप दोनों में से कोई नहीं है। मॉडल एक टूल कॉल करता है, नतीजा पढ़ता है, और तय करता है कि आगे क्या कॉल करना है, अक्सर मिलीसेकंड के भीतर। तीन पैटर्न बार-बार दिखते हैं:

  • रिट्राई स्टॉर्म। एक टूल विफल होता है, मॉडल फिर कोशिश करता है, फिर विफल होता है, और यह तब तक दोहराता है जब तक उसका कॉन्टेक्स्ट या बजट खत्म न हो जाए।
  • समानांतर फैन-आउट। क्लाइंट एक साथ कई टूल कॉल भेज सकते हैं, इसलिए एक ही प्रॉम्प्ट एक दर्जन एक साथ होने वाले अनुरोधों में बदल सकता है।
  • बेकाबू लूप। एक अस्पष्ट काम और एक ऐसा टूल जो कभी "हो गया" नहीं कहता, एक ही सेशन से सैकड़ों कॉल पैदा करता है।

इसका एक सुरक्षा पहलू भी है। कोई वेब पेज या दस्तावेज़ जिसे एजेंट पढ़ता है, उसमें ऐसे छिपे निर्देश हो सकते हैं जो उसे बार-बार एक टूल कॉल करने को कहें। आप हर injection को हमेशा नहीं रोक सकते, पर रेट लिमिट नुकसान की सीमा तय कर देती है।

टूल पैसे वाले APIs को रैप करते हैं

ज़्यादातर MCP टूल किसी ऐसी चीज़ के पतले रैपर होते हैं जिसकी कीमत लगती है या जिसका अपना कोटा होता है: एक लार्ज लैंग्वेज मॉडल, एक इमेज जनरेटर, एक सर्च API, एक डेटाबेस। एक लिमिटर एक साथ तीन चीज़ों की रक्षा करता है:

  1. आपका बजट, क्योंकि एक शोर मचाने वाला सेशन एक दिन का खर्च नहीं जलाना चाहिए।
  2. आपके अपस्ट्रीम कोटा, क्योंकि प्रदाता दुरुपयोग का जवाब 429 रिस्पॉन्स से देते हैं, जो आपके सर्वर के हर यूज़र पर असर डालते हैं।
  3. दूसरे यूज़र्स की लेटेंसी, क्योंकि एक लालची क्लाइंट जो आपके वर्कर्स को संतृप्त कर देता है, बाकी सबको धीमा कर देता है।

💡 लोकल stdio सर्वर को भी लिमिट चाहिए। भले ही सर्वर एक लैपटॉप पर सिर्फ़ एक व्यक्ति चलाए, एक लूप करता एजेंट उसके पीछे के पेड API को खाली कर सकता है। हर टूल के लिए बजट जोड़ने में कोई खर्च नहीं आता और यह एक बहुत अप्रिय सरप्राइज़ को रोकता है।

सही एल्गोरिदम चुनें

अखरोट की लकड़ी की मेज़ पर रखी काँच की रेत-घड़ी, जिसमें सुनहरी रेत आधी गिर चुकी है

छह डिज़ाइन लगभग हर स्थिति को संभालते हैं। एजेंट ट्रैफ़िक उन तक पहुँचने पर वे कैसा व्यवहार करते हैं, यहाँ है:

एल्गोरिदमबर्स्ट व्यवहारप्रति कॉलर मेमोरीसबसे उपयुक्त
फ़िक्स्ड विंडोविंडो के किनारों पर 2x तक अनुमतिएक काउंटरदैनिक कैप जैसे सरल कोटा
Sliding window logसटीक, कोई एज बर्स्ट नहींहर अनुरोध के लिए एक टाइमस्टैम्पकम वॉल्यूम, सख्त लिमिट
Sliding window counterलगभग सटीकदो काउंटरहाई-वॉल्यूम HTTP एंडपॉइंट
Token bucketनियंत्रित बर्स्ट, स्थिर रीफ़िलदो नंबरएजेंट से टूल कॉल
Leaky bucketकोई बर्स्ट नहीं, स्मूद आउटपुटएक queueनाज़ुक अपस्ट्रीम सेवाओं को डेटा देना
Concurrency limitसमानांतर जॉब पर कैपएक काउंटरलंबे चलने वाले टूल

फ़िक्स्ड और स्लाइडिंग विंडो

फ़िक्स्ड विंडो काउंटर सबसे सरल डिज़ाइन है: हर मिनट के अनुरोध गिनें और मिनट की शुरुआत पर रीसेट करें। यह सस्ता है और समझाना आसान है, पर एक कॉलर 12:00:59 पर पूरा कोटा भेज सकता है और फिर 12:01:00 पर एक और पूरा कोटा, जिससे आपके सर्वर को दिखने वाला बर्स्ट दोगुना हो जाता है।

स्लाइडिंग विंडो मौजूदा पल से पिछले 60 सेकंड देखकर यह किनारा हटा देती है। यह या तो हर टाइमस्टैम्प स्टोर करती है (सटीक, पर ज़्यादा मेमोरी लेती है) या पिछली विंडो के काउंटर को वेट देती है (काफ़ी करीब और सस्ती)। जब आपको 1,000 कॉल प्रति दिन जैसे सीधे कोटा चाहिए, जहाँ सटीक रीफ़िल का पल मायने नहीं रखता, तब विंडो चुनें।

Token Bucket आम तौर पर क्यों जीतता है

गैल्वेनाइज़्ड बाल्टी का निचला-कोण दृश्य, जो एक टपकते पीतल के नल के नीचे रखी है, पानी पर लहरें हैं

एजेंट ट्रैफ़िक बर्स्ट में आता है: दस सेकंड कुछ नहीं, फिर एक साथ छह टूल कॉल, फिर सन्नाटा। token bucket इस आकार में फ़िट होता है। हर कॉलर के पास एक बाल्टी होती है जिसकी capacity (सबसे बड़ा अनुमत बर्स्ट) और refill rate (लगातार गति) होती है। एक कॉल टोकन निकालती है, और समय उन्हें वापस डालता है। 60 टोकन की बाल्टी, जो हर सेकंड एक टोकन से भरती है, 60 कॉल का बर्स्ट अनुमति देती है, फिर हर सेकंड एक कॉल, जिसे यूज़र्स को आसानी से "अभी 60, उसके बाद प्रति मिनट 60" कहकर समझाया जा सकता है।

दो गुण इसे MCP के लिए आदर्श बनाते हैं:

  • लेज़ी रीफ़िल। रीफ़िल की गणना तब होती है जब अनुरोध आता है, इसलिए संभालने के लिए कोई टाइमर नहीं होते।
  • कॉस्ट सपोर्ट। एक वीडियो टूल 20 टोकन ले सकता है जबकि एक लुकअप एक लेता है, दोनों एक ही बजट से।

TypeScript में लिमिटर बनाएँ

डेवलपर का ऊपर से कंधे के पास से दृश्य, जो एक सुव्यवस्थित लकड़ी की मेज़ पर टाइप कर रहा है, मॉनिटर धुंधला है

नीचे दिया लिमिटर किसी भी TypeScript MCP सर्वर में काम करता है जो @modelcontextprotocol/sdk पर बना है। इसकी कोई डिपेंडेंसी नहीं है और यह अपनी स्थिति मेमोरी में रखता है।

लिमिटर क्लास

// rate-limit.ts
export type Decision = {
  allowed: boolean;
  remaining: number;
  retryAfterMs: number;
};

type Bucket = { tokens: number; updatedAt: number };

export class TokenBucket {
  private buckets = new Map<string, Bucket>();

  constructor(
    private readonly capacity: number,
    private readonly refillPerSecond: number,
  ) {}

  take(id: string, cost = 1): Decision {
    if (cost > this.capacity) {
      throw new RangeError(`Cost ${cost} is larger than the bucket (${this.capacity})`);
    }

    const now = Date.now();
    const bucket = this.buckets.get(id) ?? { tokens: this.capacity, updatedAt: now };

    const elapsedSeconds = (now - bucket.updatedAt) / 1000;
    bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsedSeconds * this.refillPerSecond);
    bucket.updatedAt = now;
    this.buckets.set(id, bucket);

    if (bucket.tokens >= cost) {
      bucket.tokens -= cost;
      return { allowed: true, remaining: Math.floor(bucket.tokens), retryAfterMs: 0 };
    }

    const missing = cost - bucket.tokens;
    return {
      allowed: false,
      remaining: 0,
      retryAfterMs: Math.ceil((missing / this.refillPerSecond) * 1000),
    };
  }

  // Drop idle buckets so the map cannot grow forever.
  sweep(maxIdleMs = 10 * 60_000) {
    const cutoff = Date.now() - maxIdleMs;
    for (const [id, bucket] of this.buckets) {
      if (bucket.updatedAt < cutoff) this.buckets.delete(id);
    }
  }
}

export const budget = new TokenBucket(60, 1); // burst of 60, refills one per second
setInterval(() => budget.sweep(), 60_000).unref();

इनमें से तीन विवरणों पर दूसरी नज़र डालने लायक है। रीफ़िल बीते समय से आता है, इसलिए हर कॉलर के लिए कोई setInterval नहीं है। cost आर्गुमेंट एक ही लिमिटर को सस्ते और महँगे दोनों टूल्स के लिए काम करने देता है। और retryAfterMs साफ़-साफ़ बताता है कि बाल्टी में पर्याप्त टोकन आने में कितना समय लगेगा, और यही वह संख्या है जो आप एजेंट को दिखाते हैं।

हर टूल हैंडलर को रैप करें

चेक को एक ही रैपर में रखें ताकि कोई टूल उसे भूल न सके:

import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { budget } from "./rate-limit.js";

type Extra = { authInfo?: { clientId?: string }; sessionId?: string };

export function limited<Args>(
  tool: string,
  cost: number,
  handler: (args: Args, extra: Extra) => Promise<CallToolResult>,
) {
  return async (args: Args, extra: Extra): Promise<CallToolResult> => {
    const caller = extra.authInfo?.clientId ?? "anonymous";
    const decision = budget.take(caller, cost);

    if (!decision.allowed) {
      const seconds = Math.ceil(decision.retryAfterMs / 1000);
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Rate limit reached for ${tool}. Wait ${seconds} seconds before calling it again.`,
          },
        ],
      };
    }
    return handler(args, extra);
  };
}

server.registerTool(
  "generate_image",
  { description: "Generate an image from a prompt", inputSchema: { prompt: z.string().max(4000) } },
  limited("generate_image", 5, async ({ prompt }) => {
    const url = await createImage(prompt);
    return { content: [{ type: "text", text: url }] };
  }),
);

💡 ब्लॉक को थ्रोन एरर की जगह टूल रिज़ल्ट के रूप में लौटाएँ। स्पेसिफ़िकेशन प्रोटोकॉल एरर को टूल एक्ज़ीक्यूशन एरर से अलग करता है। isError: true वाला रिज़ल्ट मॉडल के कॉन्टेक्स्ट के भीतर रहता है, इसलिए एजेंट "12 सेकंड इंतज़ार करें" पढ़ता है और अपना व्यवहार बदल लेता है। एक थ्रोन अपवाद JSON-RPC एरर बन जाता है, जिसे कई क्लाइंट केवल एक विफलता के रूप में दिखाते हैं और बस।

कॉलर की पहचान करें और एंडपॉइंट की रक्षा करें

स्टेनलेस स्टील के टर्नस्टाइल रीडर पर ट्रांज़िट कार्ड टैप करते हाथ का क्लोज़-अप

लिमिट उतनी ही निष्पक्ष होती है जितना उस तरीके से जिससे आप कॉलर की पहचान करते हैं। इसे गलत करें तो या तो सभी को एक साथ थ्रॉटल करेंगे, या कोई क्लाइंट दोबारा कनेक्ट होकर लिमिट को चकमा दे देगा।

सही पहचान चुनें

ट्रांसपोर्ट और ऑथइस्तेमाल करने वाली पहचानकिससे सावधान रहें
stdioहर टूल के लिए एक साझा बजटएक प्रोसेस एक ही क्लाइंट की सेवा करती है, इसलिए अलग करने के लिए कोई कॉलर नहीं है
Streamable HTTP with OAuthauthInfo से क्लाइंट या यूज़र idसबसे अच्छा विकल्प, क्योंकि यह दोबारा कनेक्ट होने पर भी बचा रहता है
Streamable HTTP with a static bearer tokenटोकन का हैशटोकन रोटेट करें और स्टोर करने से पहले उन्हें हैश करें
Anonymous HTTPIP addressसाझा ऑफ़िस और मोबाइल नेटवर्क एक ही कॉलर जैसे दिखते हैं

Mcp-Session-Id हेडर को मुख्य पहचान बनाने के लालच से बचें। सर्वर इसे जारी करता है, और एक क्लाइंट बस एक नया सेशन इनिशियलाइज़ करके नया बकेट पा सकता है। सेशन का उपयोग द्वितीयक लिमिट के रूप में करें, जैसे प्रति सेशन चल रहे जॉब्स को कैप करने के लिए, और बजट को किसी ऐसी चीज़ से जोड़कर रखें जो दोबारा कनेक्ट होने पर भी बची रहे।

429 और Retry-After के साथ लौटाएँ

सुबह की धुंध में खाली रेल पटरियों के ऊपर एक लाल रेलवे सिग्नल का टेलीफ़ोटो दृश्य

HTTP लेयर पर एक मोटी लिमिट और टूल्स के भीतर एक सटीक लिमिट लगाएँ। HTTP लेयर सस्ती है और किसी भी JSON पार्स होने या सेशन बनने से पहले चलती है, इसलिए यह सर्वर को बाढ़ से बचाती है। यह बॉडी पढ़े बिना tools/list और एक महँगे tools/call में फ़र्क नहीं कर सकती, इसलिए इसे उदार रखें और सटीक हिसाब टूल लेयर पर छोड़ें।

import { createHash } from "node:crypto";
import type { NextFunction, Request, Response } from "express";
import { TokenBucket } from "./rate-limit.js";

const httpBudget = new TokenBucket(120, 2); // 120 burst, 2 per second sustained

function callerId(req: Request): string {
  const auth = req.header("authorization");
  if (auth) return "tok:" + createHash("sha256").update(auth).digest("hex").slice(0, 16);
  return "ip:" + req.ip;
}

export function limitHttp(req: Request, res: Response, next: NextFunction) {
  const decision = httpBudget.take(callerId(req));
  res.setHeader("RateLimit-Remaining", String(decision.remaining));
  if (decision.allowed) return next();

  res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1000)));
  res.status(429).json({
    jsonrpc: "2.0",
    error: { code: -32000, message: "Too many requests. Retry after the delay in Retry-After." },
    id: null,
  });
}

// app.set("trust proxy", 1);
// app.post("/mcp", limitHttp, handleMcp);

बियरर टोकन को पहचानकर्ता के रूप में इस्तेमाल करने से पहले उसे हैश करें, ताकि कच्चे क्रेडेंशियल कभी Map या लॉग की किसी लाइन में न रहें। Retry-After को पूरे सेकंड में भेजें, क्योंकि रिट्राई लॉजिक वाले HTTP क्लाइंट उसे पढ़ते हैं। और प्रॉक्सी के पीछे हों तो trust proxy सेट करें, वरना हर अनामित कॉलर प्रॉक्सी का पता साझा करेगा।

टूल्स को उनकी लागत से तौलें

शेफ़ के हाथ किचन पास पर एक डिश सजाते हुए, स्टील की रेल पर खाली ऑर्डर टिकट लटक रहे हैं

किसी व्यस्त रेस्तराँ की रसोई में चले जाइए और आपको एक ही रेल पर बहुत अलग-अलग आकार के ऑर्डर टिकट लटकते दिखेंगे। एक साइड सलाद और धीमी ब्रेज़्ड डिश में एक जैसी मेहनत नहीं लगती, और अच्छी रसोई उन्हें एक जैसा नहीं मानती। टूल कॉल भी इसी तरह काम करती हैं।

टूल का प्रकारउदाहरणटोकन कॉस्टअतिरिक्त गार्ड
सिर्फ़-पढ़ने वाला लुकअपlist_articles, get_article1कोई नहीं
लिखना या प्रकाशित करनाsave_article2Idempotency चेक
टेक्स्ट जनरेशनलार्ज लैंग्वेज मॉडल से सारांश3आउटपुट की लंबाई कैप करें
इमेज जनरेशनgenerate_image5प्रति कॉलर 2 समानांतर
वीडियो जनरेशनgenerate_image_to_video201 समानांतर, दूरी वाले सबमिशन

हर सेकंड एक टोकन से भरने वाली 60 टोकन की बाल्टी के साथ एक कॉलर एक बर्स्ट में 60 लुकअप चला सकता है, या 12 इमेज जनरेशन, या 3 वीडियो जॉब, और बजट एक मिनट में पूरी तरह रीफ़िल हो जाता है।

टूल के अनुसार कॉस्ट वेट

वेट्स एक ही जगह रखें और उन्हें पिछले सेक्शन के रैपर को पास करें:

export const TOOL_COST = {
  list_articles: 1,
  save_article: 2,
  generate_image: 5,
  generate_image_to_video: 20,
} as const;

// limited("generate_image", TOOL_COST.generate_image, handler)

शुरुआत उन वेट्स से करें जो हर कॉल की पैसे या अपस्ट्रीम सेकंड में लागत के अनुपात में हों, फिर उन्हें असली ट्रैफ़िक से एडजस्ट करें।

जॉब्स को कैप करें और अपस्ट्रीम पर बैक ऑफ़ करें

टोकन बजट यह सीमित करता है कि कॉलर कितनी बार काम शुरू करता है। यह सीमित नहीं करता कि एक ही पल में कितना काम चलता है। लंबे टूल्स को कंकरेंसी कैप चाहिए, और साझा अपस्ट्रीम queue को कभी-कभी सबमिशन के बीच दूरी चाहिए। दोनों छोटे हैं:

const inFlight = new Map<string, number>();

export async function withConcurrency<T>(
  caller: string,
  max: number,
  job: () => Promise<T>,
): Promise<T | "busy"> {
  const current = inFlight.get(caller) ?? 0;
  if (current >= max) return "busy";

  inFlight.set(caller, current + 1);
  try {
    return await job();
  } finally {
    const left = (inFlight.get(caller) ?? 1) - 1;
    if (left <= 0) inFlight.delete(caller);
    else inFlight.set(caller, left);
  }
}

// One submission per slot: concurrent callers queue behind each other.
let nextSlot = 0;
export async function waitForSlot(minGapMs = 30_000) {
  const now = Date.now();
  const start = Math.max(now, nextSlot);
  nextSlot = start + minGapMs;
  await new Promise((resolve) => setTimeout(resolve, start - now));
}

// When the upstream API answers 429 or 5xx, wait and retry with jitter.
export async function fetchWithBackoff(
  send: () => Promise<Response>,
  maxAttempts = 4,
): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    const response = await send();
    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable || attempt >= maxAttempts) return response;

    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
    await new Promise((resolve) => setTimeout(resolve, delayMs + Math.random() * 250));
  }
}

"busy" को एजेंट को एक सामान्य टूल एरर के रूप में लौटाएँ, जो बताए कि जॉब पहले से चल रहा है और उसके स्टेटस को पोल करने का सुझाव दे। fetchWithBackoff में जिटर ज़रूरी है: उसके बिना, हर इंस्टेंस जिसे 429 मिला, ठीक एक ही पल पर जागेगा और अपस्ट्रीम को फिर से एक साथ मारेगा।

💡 असली लिमिट, सार्वजनिक रूप से प्रकाशित। PicassoIA का अपना API प्रति अकाउंट 5 एक साथ predictions दस्तावेज़ित करता है, जो API टोकन और MCP कनेक्शन में साझा होते हैं, साथ ही 4,000 कैरेक्टर के प्रॉम्प्ट, 10 MB की रिक्वेस्ट बॉडी और 3 घंटे का टाइमआउट। इसके इमेज और वीडियो टूल्स predict_id और next_poll_in_seconds संकेत भी लौटाते हैं, ताकि क्लाइंट को कभी अनुमान न लगाना पड़े कि कितनी बार पोल करना है। यह विचार अपनाएँ: एक प्रकाशित संख्या और पोलिंग संकेत वाली लिमिट वह लिमिट है जिसका एजेंट पालन कर सकते हैं।

एक प्रोसेस से आगे स्केल करें

डेटा सेंटर में काले सर्वर रैक के गलियारे का चौड़ा दृश्य

इन-मेमोरी लिमिटर में एक खामी है: उसकी मेमोरी एक ही प्रोसेस की होती है। लोड बैलेंसर के पीछे तीन इंस्टेंस चलाएँ तो हर एक अपने काउंटर रखता है, इसलिए कॉलर को असल में लिमिट का तीन गुना मिलता है। सर्वरलेस प्लेटफ़ॉर्म इससे भी बदतर हैं, क्योंकि हर कोल्ड स्टार्ट खाली बकेट से शुरू होता है। हल है काउंटर को एक साझा स्टोर में ले जाना, और Redis इसका आम विकल्प है क्योंकि उसके ऑपरेशन atomic और तेज़ हैं।

Redis के साथ साझा काउंटर

import Redis from "ioredis";
import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible";
import type { Decision } from "./rate-limit.js";

const redis = new Redis(process.env.REDIS_URL!);
const FAIL_OPEN = process.env.RATE_LIMIT_FAIL_OPEN === "true";

const limiter = new RateLimiterRedis({
  storeClient: redis,
  points: 60,   // budget per window
  duration: 60, // window length in seconds
});

export async function takeShared(caller: string, cost: number): Promise<Decision> {
  try {
    const res = await limiter.consume(caller, cost);
    return { allowed: true, remaining: res.remainingPoints, retryAfterMs: 0 };
  } catch (rejection) {
    if (rejection instanceof RateLimiterRes) {
      return { allowed: false, remaining: 0, retryAfterMs: rejection.msBeforeNext };
    }
    // Redis itself failed, so apply the configured failure policy.
    return { allowed: FAIL_OPEN, remaining: 0, retryAfterMs: 5_000 };
  }
}

यह लाइब्रेरी फ़िक्स्ड विंडो में गिनती करती है, इसलिए एल्गोरिदम टेबल वाला एज बर्स्ट लागू होता है। ज़्यादातर MCP सर्वरों के लिए यह समझौता ठीक है। अगर आपको इंस्टेंस भर में असली token bucket चाहिए, तो दोनों नंबर Redis hash में रखें और रीफ़िल और टोकन निकालने को एक ही Lua स्क्रिप्ट में चलाएँ, क्योंकि दो इंस्टेंस से पहले पढ़ना और फिर लिखना एक race है।

फ़ेल ओपन या फ़ेल क्लोज़्ड

Redis कभी न कभी ठप होगा, और आपके लिमिटर को एक पक्ष चुनना होगा:

  • पैसे खर्च करने वाले टूल्स के लिए फ़ेल क्लोज़्ड। एक छोटी आउटेज एक बिना-सीमा वाली वीडियो queue से सस्ती पड़ती है।
  • सस्ते रीड के लिए फ़ेल ओपन, जहाँ सबको ब्लॉक करना लुकअप के बर्स्ट से ज़्यादा नुकसान करेगा।
  • बंद करने के बजाय घटाएँ। लाइब्रेरी फ़ॉलबैक के रूप में एक इन-मेमोरी insuranceLimiter सपोर्ट करती है। तब लिमिट हर इंस्टेंस पर लागू होती है, जो फिर भी कोई लिमिट न होने से कहीं बेहतर है।

आप जो भी चुनें, हर लिमिटर विफलता को ज़ोर से लॉग करें, क्योंकि चुपचाप फ़ेल ओपन होना ही वह तरीका है जिससे लिमिट चुपके से खत्म हो जाती है।

लिमिट्स टेस्ट करें और देखते रहें

पीतल के प्रेशर गेज का मैक्रो दृश्य, जिसकी सुई हरे ज़ोन में टिकी है

जो लिमिटर टेस्ट में कभी कुछ अस्वीकार न करे, उस पर भरोसा नहीं किया जा सकता।

Fake Timers से टेस्ट करें

Fake timers यूनिट टेस्ट को एक मिलीसेकंड में पूरा एक मिनट पार करने देते हैं:

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TokenBucket } from "./rate-limit";

describe("TokenBucket", () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it("allows a burst, then blocks with a wait time", () => {
    const bucket = new TokenBucket(3, 1);
    for (let i = 0; i < 3; i++) expect(bucket.take("a").allowed).toBe(true);

    const blocked = bucket.take("a");
    expect(blocked.allowed).toBe(false);
    expect(blocked.retryAfterMs).toBeGreaterThan(0);
  });

  it("refills as time passes", () => {
    const bucket = new TokenBucket(1, 1);
    bucket.take("a");
    expect(bucket.take("a").allowed).toBe(false);

    vi.advanceTimersByTime(1000);
    expect(bucket.take("a").allowed).toBe(true);
  });

  it("keeps callers apart", () => {
    const bucket = new TokenBucket(1, 1);
    bucket.take("a");
    expect(bucket.take("b").allowed).toBe(true);
  });
});

यूनिट टेस्ट के बाद, MCP Inspector (npx @modelcontextprotocol/inspector) के तहत असली सर्वर चलाएँ और एक टूल को लूप में कॉल करें। पहली कॉलें सफल होनी चाहिए और बाकी प्रतीक्षा समय के साथ रेट लिमिट संदेश लौटाएँ। HTTP लेयर टेस्ट करने के लिए curl से बर्स्ट भेजें और स्टेटस कोड गिनें:

for i in $(seq 1 150); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/mcp \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
done | sort | uniq -c

शुरुआती रिस्पॉन्स आपके सेशन सेटअप पर निर्भर करते हैं, पर एक बार बकेट खाली हो जाए, तो हर रिस्पॉन्स 429 होना चाहिए।

देखने योग्य मेट्रिक्स

हर अस्वीकृति को टूल के नाम और कॉलर बकेट के साथ गिनें (कच्ची पहचान कभी नहीं)। ये कुछ संख्याएँ बताती हैं कि लिमिट निष्पक्ष हैं या नहीं:

मेट्रिकयह आपको क्या बताता हैअलर्ट कब करें
प्रति टूल रिजेक्शन दरलिमिट बहुत कसी हैं, या कोई एक शोर मचाने वाला कॉलर10 मिनट तक 5% से ऊपर
अस्वीकृतियों वाले शीर्ष कॉलरकोई लूप करता या दुरुपयोग करता एजेंटएक कॉलर उनमें से आधे से ज़्यादा पर कब्ज़ा रखे
Retry-After, 95वाँ पर्सेंटाइलएजेंट असल में कितनी देर इंतज़ार कर रहे हैं60 सेकंड से ऊपर
चल रहे जॉब्सकंकरेंसी संतृप्ति5 मिनट तक कैप पर रहे
लिमिटर एररRedis की सेहतकोई भी

असली सर्वरों में दिखने वाली गलतियाँ:

  • सेशन ID को अकेली पहचान बनाना, ताकि दोबारा कनेक्ट होने पर लिमिट रीसेट हो जाए।
  • एक साझा ग्लोबल बकेट रखना, ताकि एक भारी कॉलर सबको भूखा रखे।
  • अनुरोधों को चुपचाप गिराना या रोकना, बजाय प्रतीक्षा समय वाली एरर लौटाने के।
  • लिमिट एक बार सेट करना और फिर कभी अस्वीकृति की संख्याएँ न पढ़ना।

PicassoIA पर आज़माएँ

ऊपर का लिमिटर सौ लाइनों से कम का है, जो उसे एक language model के लिए अच्छा काम बनाता है: सख्ती से स्पेसिफ़ाइड, टेस्ट करने में आसान और जल्दी रिव्यू होने वाला। PicassoIA कई coding-सक्षम मॉडल एक ही जगह रखता है, ताकि आप अलग-अलग अकाउंट संभाले बिना ड्राफ़्ट, तुलना और सुधार कर सकें।

Claude Sonnet 5 का उपयोग कैसे करें

  1. मॉडल खोलें। PicassoIA पर Claude Sonnet 5 पर जाएँ।
  2. विशिष्ट प्रॉम्प्ट चिपकाएँ। SDK, ट्रांसपोर्ट, एल्गोरिदम, बजट, हर टूल की लागत और सटीक एरर टेक्स्ट बताएँ। उदाहरण के लिए:
Write a TypeScript token bucket limiter for an MCP server built on
@modelcontextprotocol/sdk. Budget: 60 tokens, refill 1 per second.
Costs: list_articles 1, generate_image 5, generate_image_to_video 20.
Identify callers by authInfo.clientId, fall back to "anonymous".
When blocked, return isError: true with the wait time in seconds.
Include vitest tests that use fake timers.
  1. पहले टेस्ट माँगें। टेस्ट पढ़ने से पता चलता है कि मॉडल ने इम्प्लीमेंटेशन की एक लाइन पढ़ने से पहले कौन-सा व्यवहार मान लिया था।
  2. उन्हें चलाएँ और विफलताएँ वापस दें। सटीक एरर आउटपुट उसी बातचीत में चिपकाएँ और सुधार माँगें।
  3. रिव्यू का अनुरोध करें। "Review this limiter for race conditions and memory growth" के साथ खत्म करें और जवाब को आलोचनात्मक नज़र से पढ़ें।

हर अनुरोध को एक चिंता तक सीमित रखें, और "उचित डिफ़ॉल्ट" की जगह मॉडल को अपनी असली संख्याएँ दें। कुछ दूसरे मॉडल दूसरी राय के लायक हैं:

मॉडलइसका उपयोग करें
GPT 5.6 Solजटिल concurrency और Lua स्क्रिप्ट लॉजिक की जाँच
Kimi K2.6एजेंट की ओर वाले retry और backoff कोड का ड्राफ़्ट
Gemini 3.5 Flashतेज़ इटरेशन और सैंपल टेस्ट डेटा

एक बार आपका सर्वर सुरक्षित हो जाए, तो उसे काम पर लगाएँ। इस लेख की हर तस्वीर एक सादे टेक्स्ट प्रॉम्प्ट से शुरू हुई जो P-Image के लिए लिखा गया था, और आप वही प्रॉम्प्ट Flux 2 Pro पर चलाकर नतीजों की तुलना कर सकते हैं। Picasso IA खोलें, एक-दो वाक्यों में एक दृश्य बताएँ और अपनी पहली इमेज जनरेट करें। लेंस, रोशनी या कोण बदलें, फिर से चलाएँ, और फिर अपने पसंदीदा नतीजे को एक छोटे वीडियो में एनिमेट करें। यह जानने का सबसे तेज़ तरीका है कि प्लेटफ़ॉर्म क्या कर सकता है, अपने ही विचारों के साथ प्रयोग करना।

यह लेख शेयर करें

अपनी भाषा चुनें

संबंधित लेख