MCP Forge

How to rate limit an MCP server

A practical guide. Updated 2026.

An MCP server is called by a language model, not a human clicking a button. That changes the rate-limiting math. A model in an agent loop can fire the same tool dozens of times in a second, retry on every error, or get stuck in a cycle where each tool result prompts another call. If one of your tools hits a paid API, a database, or an outbound fetch, an unbounded caller turns into a runaway bill or a denial of service against your own backend. Rate limiting is the seatbelt that keeps a misbehaving client from taking the whole server down.

Why MCP servers need it more than a normal API

A REST API mostly sees traffic shaped by real users. An MCP server sees traffic shaped by an LLM that has no instinct for cost and will happily retry forever. Three patterns make this acute:

So you are protecting two things at once: your upstream dependencies, and the client's own budget.

Per-client token buckets

The token bucket is the right primitive here. Each client gets a bucket with a maximum capacity (the burst) that refills at a fixed rate (the sustained rate). Every call removes one token. If the bucket is empty, the call is rejected or made to wait. This allows short bursts while capping the long-run average, which fits agent traffic well: a flurry of calls is fine, a sustained flood is not.

The key word is per-client. A single global limit means one noisy agent starves everyone else. You need to key the bucket on something stable. In rough order of preference:

Here is a minimal, correct token bucket keyed per client. It uses lazy refill, so you do not need a background timer.

class TokenBucket {
  constructor(capacity, refillPerSec) {
    this.capacity = capacity;
    this.refillPerSec = refillPerSec;
    this.tokens = capacity;
    this.last = Date.now();
  }
  take(n = 1) {
    const now = Date.now();
    const elapsed = (now - this.last) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSec);
    this.last = now;
    if (this.tokens >= n) { this.tokens -= n; return true; }
    return false;
  }
}

const buckets = new Map();
function allow(clientKey) {
  let b = buckets.get(clientKey);
  if (!b) { b = new TokenBucket(20, 5); buckets.set(clientKey, b); } // burst 20, 5/sec
  return b.take();
}

That gives each client a burst of 20 calls and a steady 5 calls per second. For multi-process or serverless deployments, move the counter into Redis (the INCR plus EXPIRE pattern, or a Lua token-bucket script) so the limit holds across instances. An in-memory map only works for a single process.

Where to enforce it: edge vs per-tool

You generally want both, at different granularities.

The cleanest place to put the edge check is a wrapper around your request handler. Reject early, before any work happens, and return a proper error rather than crashing.

async function handleToolCall(req, ctx) {
  const key = ctx.clientId ?? ctx.sessionId ?? "anon";

  // Edge limit: every call counts.
  if (!allow("edge:" + key)) {
    throw new McpError(-32000, "Rate limit exceeded. Slow down and retry.");
  }
  // Per-tool limit: only the expensive ones.
  const costly = new Set(["web_search", "send_email", "run_query"]);
  if (costly.has(req.tool) && !allow("tool:" + req.tool + ":" + key)) {
    throw new McpError(-32000, `Rate limit for ${req.tool}. Try again shortly.`);
  }
  return runTool(req.tool, req.args, ctx);
}

Return errors the model can act on

An HTTP API returns 429 and a Retry-After header. MCP travels over JSON-RPC, so the model sees your error message, not the status code. Make the message tell the model what to do. "Rate limit exceeded, retry in 2 seconds" is far better than a generic failure, because the model reads it and can back off instead of hammering. A vague error often causes the exact retry storm you were trying to prevent.

Common mistakes to check for

That last one is worth a scan of your config. The free mcp-audit tool flags remote MCP servers exposed without authentication or over cleartext, the same servers that tend to ship with no rate limiting either. It runs locally with zero dependencies, so it is a quick first check before you ship.

Ship rate limiting without writing it from scratch

MCP Forge Kit includes a per-client token-bucket limiter with edge and per-tool enforcement, Redis support, and tests, wired into a hardened server template.

Get MCP Forge Kit, €39

Related: Authentication · Write a secure tool · Security checklist