{"record":{"id":"816db97e0b607eff","repo":"CherryHQ/cherry-studio","slug":"rate-limit-exceeded","errorCode":null,"errorMessage":"Rate limit exceeded","messagePattern":"Rate limit exceeded","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"src/main/ai/mcp/servers/braveSearch.ts","lineNumber":84,"sourceCode":"const RATE_LIMIT = {\n  perSecond: 1,\n  perMonth: 15000\n}\n\nconst requestCount = {\n  second: 0,\n  month: 0,\n  lastReset: Date.now()\n}\n\nfunction checkRateLimit() {\n  const now = Date.now()\n  if (now - requestCount.lastReset > 1000) {\n    requestCount.second = 0\n    requestCount.lastReset = now\n  }\n  if (requestCount.second >= RATE_LIMIT.perSecond || requestCount.month >= RATE_LIMIT.perMonth) {\n    throw new Error('Rate limit exceeded')\n  }\n  requestCount.second++\n  requestCount.month++\n}\n\ninterface BraveWeb {\n  web?: {\n    results?: Array<{\n      title: string\n      description: string\n      url: string\n      language?: string\n      published?: string\n      rank?: number\n    }>\n  }\n  locations?: {\n    results?: Array<{","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/braveSearch.ts#L66-L102","documentation":"Thrown by `checkRateLimit` in the Brave Search MCP server — a plain `Error`, not an McpError. It fires when `requestCount.second >= 1` (perSecond is 1) OR `requestCount.month >= 15000` (perMonth). Two important implementation details: (1) the per-second counter resets only if more than 1000ms elapsed since `lastReset`, so any two calls within the same second trip it; (2) the per-MONTH counter is initialized once at module load and NEVER reset anywhere in the code — it is a monotonic accumulator for the whole Electron process lifetime, so once it crosses 15000 every subsequent call fails permanently until the app restarts. Worse, a single `brave_local_search` fans out into multiple rate-checked calls (performLocalSearch itself, plus getPoisData and getDescriptionsData in parallel, and a performWebSearch fallback when there are no local results), so one logical local search can hit the perSecond=1 limit internally even with no external concurrency.","triggerScenarios":"Two brave search calls within the same second; any `brave_local_search` that returns local results (it makes 3 checkRateLimit calls: web lookup + POI + descriptions, the latter two in parallel within the same second); 15000 cumulative calls across the process lifetime.","commonSituations":"Burst web searches from an agent loop; local search almost always self-tripping the per-second limit on its parallel POI/description fetches; a long-running desktop session accumulating toward the 15000 ceiling and then hard-failing until restart.","solutions":["Serialize Brave Search calls with at least 1 second between them, and remember one local search counts as up to 3-4 internal rate checks.","Restart the Electron app to reset the never-reset month counter once it approaches 15000.","If you control the source, raise `RATE_LIMIT.perSecond` (braveSearch.ts:67) — a value of 1 makes local search reliably fail; the real Brave per-second allowance is higher.","Add a month-counter reset (e.g. track the calendar month) — the current code never resets it, which is a latent bug."],"exampleFix":"// before (local search self-trips the 1/sec limit)\nawait client.callTool('brave_local_search', { query: 'pizza near Central Park' })\n// after (space calls >= 1s, and prefer web search to avoid internal fan-out)\nawait client.callTool('brave_web_search', { query: 'pizza near Central Park' })","handlingStrategy":"retry","validationCode":"// Serialize Brave calls with >= 1s spacing; account for local-search fan-out.\nconst BRAVE_MIN_INTERVAL_MS = 1100 // local search fans out to ~3 internal checks\nlet lastBraveCall = 0\nasync function braveGate() {\n  const wait = BRAVE_MIN_INTERVAL_MS - (Date.now() - lastBraveCall)\n  if (wait > 0) await new Promise((r) => setTimeout(r, wait))\n  lastBraveCall = Date.now()\n}","typeGuard":"null","tryCatchPattern":"// braveSearch converts thrown errors to an isError result (braveSearch.ts:359-369).\nasync function braveSearchWithBackoff(client: Client, name: string, args: unknown, attempts = 3) {\n  for (let i = 0; i < attempts; i++) {\n    await braveGate()\n    const res = await client.callTool(name, args)\n    if (!res.isError) return res\n    if (!/Rate limit exceeded/.test(res.content[0].text)) return res\n    await new Promise((r) => setTimeout(r, 1100 * (i + 1)))\n  }\n  throw new Error('Brave rate limit persisted after retries')\n}","preventionTips":["Prefer brave_web_search over brave_local_search — local search makes up to 3-4 internal rate-checked calls and reliably self-trips the perSecond=1 limit.","Space all Brave calls at least 1 second apart and serialize them within a single client.","Restart the Electron app periodically: the per-month counter never resets in-process and will eventually hard-fail every call.","If you maintain this server, consider raising RATE_LIMIT.perSecond and adding a real month-boundary reset."],"tags":["mcp","brave-search","rate-limit","network"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}