{"record":{"id":"bca6a1f9b9d42da2","repo":"apache/shardingsphere","slug":"completion-request-limit-of-d-per-minute-exceeded","errorCode":null,"errorMessage":"Completion request limit of %d per minute exceeded for MCP session `%s`; retry after the current 60-second window ends.","messagePattern":"Completion request limit of (.+?) per minute exceeded for MCP session `(.+?)`; retry after the current 60-second window ends\\.","errorType":"validation","errorClass":"MCPUnavailableException","httpStatus":null,"severity":"warning","filePath":"mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionRateLimiter.java","lineNumber":59,"sourceCode":"    private final Clock clock;\n    \n    private final Map<String, CompletionWindow> sessionWindows = new ConcurrentHashMap<>();\n    \n    MCPCompletionRateLimiter() {\n        this(MCPRuntimeProtectionPolicy.getMaxCompletionRequestsPerMinute(), Clock.systemUTC());\n    }\n    \n    void acquire(final String sessionId) {\n        Instant now = clock.instant();\n        sessionWindows.compute(sessionId, (ignored, currentWindow) -> acquire(sessionId, currentWindow, now));\n    }\n    \n    private CompletionWindow acquire(final String sessionId, final CompletionWindow currentWindow, final Instant now) {\n        if (null == currentWindow || !now.isBefore(currentWindow.startedAt().plus(WINDOW_DURATION))) {\n            return new CompletionWindow(now, 1);\n        }\n        if (currentWindow.requestCount() >= maxRequestsPerWindow) {\n            throw new MCPUnavailableException(String.format(\n                    \"Completion request limit of %d per minute exceeded for MCP session `%s`; retry after the current 60-second window ends.\", maxRequestsPerWindow, sessionId));\n        }\n        return new CompletionWindow(currentWindow.startedAt(), currentWindow.requestCount() + 1);\n    }\n    \n    void releaseSession(final String sessionId) {\n        sessionWindows.remove(sessionId);\n    }\n    \n    private record CompletionWindow(Instant startedAt, int requestCount) {\n    }\n}\n","sourceCodeStart":41,"sourceCodeEnd":72,"githubUrl":"https://github.com/apache/shardingsphere/blob/e952770a215630a3659c75d64369168cd3e26b82/mcp/core/src/main/java/org/apache/shardingsphere/mcp/core/completion/MCPCompletionRateLimiter.java#L41-L72","documentation":"MCPCompletionRateLimiter enforces a fixed window of max completion requests per 60 seconds per session; the exception (MCPUnavailableException) is thrown when a session's requestCount already reached maxRequestsPerWindow inside the current window. The window is keyed by sessionId and resets when now >= startedAt + 60s, or when releaseSession removes the session entry. The message includes the configured limit and the offending session id and instructs the client to retry after the window ends.","triggerScenarios":"Issuing more completion/complete requests than the configured limit for one session within a sliding start-of-window 60-second period, e.g. an LLM client that fans out one completion per keystroke or per argument without client-side throttling.","commonSituations":"Aggressive editor-style autocomplete wired to the MCP completion tool; batch scripts looping completion calls; low maxRequestsPerWindow configured server-side; a stale window not released after a session closed (missing releaseSession) making the next session's count appear exhausted only if ids collide — normally just genuine overuse.","solutions":["Throttle client-side to the configured N requests per 60 seconds per session (debounce completion triggers).","Wait until the current 60-second window closes, then retry; the counter resets at window boundaries.","Ask the operator to raise maxRequestsPerWindow if legitimate workloads exceed it.","Call releaseSession (server closes session) when done so window state is cleaned up."],"exampleFix":"// before: fire a completion on every input event\ninput.onChange(() => mcp.callTool('completion', args)); // exceeds N/min -> 429-style error\n\n// after: debounce and cap to the server limit\nlet pending = 0;\ninput.onChange(debounce(async () => {\n  if (pending >= LIMIT_PER_MIN) return; // wait for window reset\n  pending++;\n  try { await mcp.callTool('completion', args); } finally {}\n}, 250));","handlingStrategy":"retry","validationCode":"// Client-side fixed-window limiter matching the server contract\nfunction makeLimiter(max, windowMs = 60_000) {\n  let startedAt = 0, count = 0;\n  return {\n    tryAcquire(now = Date.now()) {\n      if (now - startedAt >= windowMs) { startedAt = now; count = 0; }\n      return count++ < max;\n    },\n    retryAfterMs(now = Date.now()) { return Math.max(0, startedAt + windowMs - now); }\n  };\n}\nconst limiter = makeLimiter(SERVER_COMPLETION_LIMIT);\nif (!limiter.tryAcquire()) scheduleRetry(limiter.retryAfterMs());","typeGuard":null,"tryCatchPattern":"try {\n  return await mcp.callTool('completion', args);\n} catch (e) {\n  if (/Completion request limit of \\d+ per minute exceeded/.test(e.message)) {\n    await sleep(msUntilNextMinuteBoundary());  // window is a fixed 60s from first request\n    return mcp.callTool('completion', args);\n  }\n  throw e;\n}","preventionTips":["Debounce completion triggers (250-500ms) before calling the tool.","Mirror the server's N-per-60s limit client-side and back off at the boundary.","Cache completion results for identical prefixes within a window.","Close sessions when finished so server-side window state is released."],"tags":["mcp","rate-limit","completion","throttling","session"],"backgroundTag":null,"analyzedSha":"e952770a215630a3659c75d64369168cd3e26b82","analyzedAt":"2026-08-14T13:54:53.392Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}