目次
なぜ2026年のMCPにレート制限が必須なのか
MCPサーバーが「ただAPIをラップするだけ」だった2025年と違い、2026年のMCPは 複数エージェントが並列に呼び出す共有インフラ になった。1つのMCPインスタンスにClaude・GPT・Gemini・社内エージェントが同時接続する事例が珍しくなくなり、1台のエージェントの暴走が他エージェントの体験を破壊するリスクが現実化している。
業界レポートでは、ループに陥ったエージェント1台が毎分1,000を超える呼び出しを撃ち込む事例が報告されている。レート制限なしのMCPは、(1) 上流SaaSのクォータを瞬時に食い潰し、(2) クラウドコストが想定の数倍に膨張し、(3) 上流APIから一時的にBANされ全エージェントが停止する、という三重苦に陥る。
MCPサーバーは「エージェントの暴走を吸収する膜」になった。レート制限・タイムアウト・サーキットブレーカーはオプションではなく、共有インフラとしての最低要件。✅ Splunk MCP Server v1.1.0(2026年4月時点でβ提供)もグローバル+ツール単位のレート制限を実装している。
エラーフォーマット — 429とJSON-RPC両対応
レート制限を実装する前に、「どう返すか」の出口を決める必要がある。MCPは Streamable HTTP / stdio / WebSocket を許容するため、トランスポート別に分岐する。
HTTPトランスポート: 429 + Retry-After
業界推奨は「HTTP 429 Too Many Requests」+「Retry-Afterヘッダ」の組み合わせ。Retry-Afterは秒数(整数)かHTTP-date形式を受け取り、エージェント側はこの値に基づいてバックオフを起動する。
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"error": {
"code": "rate_limit_exceeded",
"message": "Per-tool rate limit hit on 'create_invoice'.",
"scope": "tool",
"tool": "create_invoice",
"retry_after_seconds": 12,
"limit": "10 requests / 60s"
}
}
stdio / 非HTTPトランスポート: JSON-RPC error
stdio経由のMCPでは、JSON-RPCのerrorオブジェクトにretry_afterを含める。エージェント側ライブラリ(MCP SDK)はこの値を読み取り、自動再試行に渡す。
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32029,
"message": "Rate limit exceeded",
"data": {
"scope": "global",
"retry_after_seconds": 8,
"limit": "100 requests / 60s",
"current_usage": 100
}
}
}
ポイントは 「scope」 をエージェントに伝えること。グローバル制限なら「ツールを切り替えても無駄」、ツール固有なら「他ツールに切り替えて続行可能」と判断できる。
サーバー側パターン1: トークンバケット
「普段は緩く、急なバーストにも一定範囲で対応したい」場合の標準解。バケット容量がバースト許容量、補充レートが平均レートを表現する。エージェントの自然な使用パターン(数秒間に集中して呼び出し → アイドル)と相性が良い。
// TypeScript / Node.js (簡易版・本番は分散ロック付きRedis推奨)
class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private capacity: number, // バースト最大値
private refillRate: number, // 1秒あたり補充トークン数
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
tryConsume(): { ok: boolean; retryAfter?: number } {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.capacity,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
if (this.tokens >= 1) {
this.tokens -= 1;
return { ok: true };
}
const needed = 1 - this.tokens;
const retryAfter = Math.ceil(needed / this.refillRate);
return { ok: false, retryAfter };
}
}
// 使用例: 1分100リクエスト、最大バースト20
const bucket = new TokenBucket(20, 100 / 60);
マルチインスタンス本番では、tokensをRedisの単一キーに置き、SET key value NX PXとINCRBYで原子的に更新するのが定石。Cloudflare WorkersならDurable Objects、AWSならElastiCache + Lua scriptが現実解だ。
サーバー側パターン2: スライディングウィンドウ
「直近N秒で必ずM回以下」を厳格に守りたい場合。上流APIのハードリミットを絶対に超えてはいけないユースケース(請求発行・送金など)で使う。
// Redis Sorted Set ベース (擬似コード)
async function checkSlidingWindow(
key: string,
windowSeconds: number,
maxRequests: number,
): Promise<{ ok: boolean; retryAfter?: number }> {
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
// 1. 古いエントリを削除
await redis.zremrangebyscore(key, 0, windowStart);
// 2. 現在のカウントを取得
const count = await redis.zcard(key);
if (count >= maxRequests) {
// 最古エントリの再試行可能時刻を計算
const oldest = await redis.zrange(key, 0, 0, "WITHSCORES");
const retryAfter = Math.ceil(
(parseInt(oldest[1]) + windowSeconds * 1000 - now) / 1000
);
return { ok: false, retryAfter };
}
// 3. 現在のリクエストを追加
await redis.zadd(key, now, `${now}-${crypto.randomUUID()}`);
await redis.expire(key, windowSeconds);
return { ok: true };
}
2026年のMCPでは 「ツール単位はトークンバケット、グローバルはスライディングウィンドウ」のハイブリッド構成 が典型解。前者はバースト対応の柔軟性、後者は上流API保護の厳格性を担う。
エージェント側: Full Jitter付き指数バックオフ
サーバー側で429を返しても、エージェントが愚直に再試行を撃ち続ければ意味がない。エージェント側に ジッター付き指数バックオフ を実装するのが必須だ。
なぜ単純な指数バックオフではダメか
10台のエージェントが同時に429を受けたとする。単純な指数バックオフ(1s, 2s, 4s, 8s...)だと、10台全員が同じスケジュールで再試行するため、ピークが同期して再発する。これが thundering herd問題。
Full Jitter方式 — AWS推奨
AWS Architecture Blogが古くから推奨する sleep = random(0, base * 2^attempt) 方式。乱数で揺らぎを入れ、再試行を時間軸に分散させる。
// TypeScript (Anthropic SDK 等にも採用されているパターン)
async function callWithBackoff<T>(
fn: () => Promise<T>,
options: { maxRetries?: number; baseMs?: number; capMs?: number } = {}
): Promise<T> {
const maxRetries = options.maxRetries ?? 5;
const baseMs = options.baseMs ?? 500;
const capMs = options.capMs ?? 30_000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
// 429 か 5xx のみ再試行対象
const status = err.status ?? err.response?.status;
if (status !== 429 && (status < 500 || status > 599)) {
throw err; // 4xx (except 429) は即失敗
}
if (attempt === maxRetries) throw err;
// サーバー指定 Retry-After を優先
const retryAfter = err.headers?.["retry-after"];
let delay: number;
if (retryAfter) {
delay = parseInt(retryAfter) * 1000;
} else {
// Full Jitter: random(0, base * 2^attempt) を cap で頭打ち
const exp = Math.min(capMs, baseMs * Math.pow(2, attempt));
delay = Math.floor(Math.random() * exp);
}
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error("unreachable");
}
4xx(429以外)は即座に失敗させる。サーバーが「リクエスト自体が間違っている」と返したエラーを再試行しても成功しない。再試行で意味があるのは 429・5xx・ネットワークタイムアウトのみ。
サーキットブレーカーとの併用
レート制限とサーキットブレーカーは守る対象が違う。
| 仕組み | 守る対象 | 発動条件 | 主な目的 |
|---|---|---|---|
| レート制限 | 自分(MCPサーバー / 上流API) | 単位時間のリクエスト数超過 | 撃ち込みすぎを防ぐ |
| サーキットブレーカー | 上流API | 連続失敗が閾値超え | 落ちている相手に追い討ちしない |
典型的なサーキットブレーカーは open / half-open / closed の3状態を持つ。MCP本番運用の出発点としては「5xx連続5回でopen、30秒後にhalf-openでサンプル1〜2件試行、成功すればclosed」が現実的。
class CircuitBreaker {
private state: "closed" | "open" | "half-open" = "closed";
private failures = 0;
private openedAt = 0;
constructor(
private threshold: number = 5,
private resetMs: number = 30_000,
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.openedAt > this.resetMs) {
this.state = "half-open";
} else {
throw new Error("circuit_open");
}
}
try {
const r = await fn();
this.failures = 0;
this.state = "closed";
return r;
} catch (err: any) {
const status = err.status ?? err.response?.status;
if (status >= 500 && status <= 599) {
this.failures += 1;
if (this.failures >= this.threshold) {
this.state = "open";
this.openedAt = Date.now();
}
}
throw err;
}
}
}
監視と運用 — 失敗から学ぶメトリクス
「何が拒否されているか」が見えなければレート制限は調整不能になる。最低限取るべきメトリクスは以下。
- throttling_rate — 全リクエストに対する429比率(目標: 1%未満)
- throttle_by_tool — ツール単位の拒否数(偏りがあれば設定見直し)
- retry_attempt_distribution — エージェント側の再試行回数分布(3回超過が多ければ容量不足)
- p95_response_time — レート制限チェックのオーバーヘッド(目標: 5ms未満)
- circuit_open_events — サーキットブレーカーが開いた回数(上流API側の問題シグナル)
Prometheus + Grafana / Cloudflare Analytics / AWS CloudWatch どれでもよいが、「ツール×エージェントID」で2軸切り出せることが運用の必須条件だ。「freee MCPのcreate_invoiceがエージェントXに対して頻繁に拒否されている」が見えて初めて、設定ミスかエージェントの実装バグかの切り分けができる。
本番チェックリスト
レート制限を本番投入する前の最低限チェックリスト。
- HTTPトランスポートでは429 + Retry-Afterヘッダを返す
- stdio/JSON-RPCではerror.dataにretry_after_secondsとscope(global/tool)を含める
- ツール単位とグローバルの2層構成(ハイブリッド)になっている
- エージェント側ライブラリにFull Jitter付き指数バックオフが実装されている
- 4xx(429以外)は再試行しない設計になっている
- サーキットブレーカーが上流API保護のため動いている
- throttling_rate / throttle_by_tool / retry_attempt_distribution が可観測
- 分散環境ではRedis等で原子的にカウンタ更新している(in-memoryではない)
- レート制限値が
environment variablesで運用中に調整可能 - テスト環境で「1台のエージェントが暴走したケース」のシミュレーションを実施済み
FAQ
Q1. なぜMCPサーバーにレート制限が必要か?
2026年のMCPは複数エージェントが共有するインフラに進化した。1台のエージェントがループに陥ると毎分1,000リクエストを超える事例があり、放置すれば(1)他エージェントの体験崩壊、(2)コスト急増、(3)上流APIからのBAN、の三重苦になる。
Q2. レート制限エラーはどう返すべきか?
HTTPトランスポートでは「HTTP 429 Too Many Requests + Retry-Afterヘッダ」が業界推奨。stdio/非HTTPではJSON-RPCのerror.dataにretry_after_secondsとscope(globalかtoolか)を入れる。エージェント側はこの値で次の再試行タイミングと戦略を決める。
Q3. なぜジッター(乱数揺らぎ)が必要か?
ジッターなしの指数バックオフは「thundering herd問題」を起こす。複数エージェントが同時に429を受けると、全員が同じスケジュール(1s→2s→4s)で再試行するためピークが同期して再発する。AWS推奨のFull Jitter方式 sleep = random(0, base * 2^attempt) で時間軸に分散させる。
Q4. トークンバケットとスライディングウィンドウ、どちらを使うか?
用途で使い分け。トークンバケットは「普段緩く、バーストにも対応」したい場合。スライディングウィンドウは「直近N秒で必ずM回以下」を厳格に守る必要がある場合。MCPでは「ツール単位はトークンバケット、グローバルはスライディングウィンドウ」のハイブリッドが2026年の典型構成。
Q5. サーキットブレーカーとの違いは?
守る対象が違う。レート制限は「自分が撃ち込みすぎないため」、サーキットブレーカーは「落ちている上流APIに追い討ちしないため」。両方を組み合わせるのが正解で、開発の出発点としては「5xx連続5回でopen、30秒後にhalf-open」が現実的。
Q6. in-memoryのトークンバケットでも本番で使えるか?
単一インスタンスの開発・PoCなら可。本番で複数インスタンスにスケールする場合は、Redis(Durable Objects/ElastiCache)経由の原子操作が必須。in-memoryのままだと、各インスタンスで上限が独立カウントされるため、全体の制限が3倍・5倍に膨らんでしまう。
本記事に含まれる「ループに陥ったエージェントが毎分1,000リクエスト超」の数値はMintMCP Blog(mintmcp.com/blog/rate-limiting-with-mcp)が公表した業界推定値。MCPレート制限実装パターン(token bucket / sliding window / 429 + Retry-After / JSON-RPC retry-after)はFast.io資料(fast.io/resources/mcp-server-rate-limiting/)、WebScraping.AI FAQ、Splunk MCP Server v1.1.0公式ドキュメント(2026年4月時点β機能)に基づく。Full Jitter方式はAWS Architecture Blog "Exponential Backoff And Jitter"が古典的な参照元。コード例は実装の概念示唆を目的とした擬似コードで、本番投入時はマルチインスタンス整合・タイムゾーン・モニタリング統合を含めた検証が必要。価格・仕様は予告なく変更される可能性があるため、本番運用時は最新の公式ドキュメントをご確認ください。