{"record":{"id":"617bc4dd25526112","repo":"ruvnet/ruflo","slug":"mcp-server-server-name-is-in-cooldown-http","errorCode":null,"errorMessage":"MCP server \"${server.name}\" is in cooldown (HTTP ${cd.status ?? \"n/a\"}, ${Math.round(remaining / 1000)}s remaining): ${cd.message}","messagePattern":"MCP server \"(.+?)\" is in cooldown \\(HTTP (.+?), (.+?)s remaining\\): (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/server/mcp/clientPool.ts","lineNumber":111,"sourceCode":"\t\tif (Number.isFinite(seconds) && seconds > 0) {\n\t\t\treturn Math.min(seconds * 1000, MAX_RATE_LIMIT_COOLDOWN_MS);\n\t\t}\n\t}\n\treturn DEFAULT_RATE_LIMIT_COOLDOWN_MS;\n}\n\nfunction checkCooldown(key: string, server: McpServerConfig): void {\n\tconst cd = failureCooldown.get(key);\n\tif (!cd) return;\n\tconst remaining = cd.until - Date.now();\n\tif (remaining <= 0) {\n\t\tfailureCooldown.delete(key);\n\t\treturn;\n\t}\n\tif (cd.status === 429) {\n\t\tthrow new McpRateLimitedError(server.name, cd.status, remaining, cd.message);\n\t}\n\tthrow new Error(\n\t\t`MCP server \"${server.name}\" is in cooldown (HTTP ${cd.status ?? \"n/a\"}, ` +\n\t\t\t`${Math.round(remaining / 1000)}s remaining): ${cd.message}`\n\t);\n}\n\nfunction recordFailure(key: string, status: number | undefined, err: unknown): void {\n\tconst message = err instanceof Error ? err.message : String(err);\n\tconst cooldownMs = status === 429 ? extractRetryAfterMs(err) : DEFAULT_RATE_LIMIT_COOLDOWN_MS;\n\tfailureCooldown.set(key, {\n\t\tuntil: Date.now() + cooldownMs,\n\t\tstatus,\n\t\tmessage,\n\t});\n}\n\nexport async function getClient(server: McpServerConfig, signal?: AbortSignal): Promise<Client> {\n\tconst key = keyOf(server);\n\tconst existing = pool.get(key);","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/mcp/clientPool.ts#L93-L129","documentation":"Thrown by checkCooldown when getClient is called for a server whose cooldown record is still active AND the recorded HTTP status was anything other than 429 (e.g. 401, 403, 404, 500, 502, 503). It surfaces a generic Error with the server name, recorded status (or 'n/a' if unknown), seconds remaining, and the original error message. recordFailure uses DEFAULT_RATE_LIMIT_COOLDOWN_MS (5s) for these non-429 statuses.","triggerScenarios":"A prior getClient attempt failed with a definitive 4xx/5xx status via the StreamableHTTP transport (statusFromTransportError returned a number in 400-599, not 408); recordFailure stored it; a subsequent call within the 5-second window hits checkCooldown and rethrows the cooldown summary instead of reconnecting.","commonSituations":"Wrong MCP server URL (404) or auth (401/403) being retried in a tight loop; upstream 500/503 during an incident; a polling/health-check loop that does not respect the cooldown; CI tests that fire getClient repeatedly against a misconfigured stub.","solutions":["Inspect the embedded status and message; for 401/403/404 fix the server config (URL, headers, credentials) rather than retrying.","For 5xx transient failures, wait out the 5s cooldown (or call evictFromPool(server) once you have reason to believe the upstream recovered).","Guard your call sites to avoid hot retry loops; respect the cooldownSeconds embedded in the message."],"exampleFix":"// before\nconst client = await getClient(server);\n\n// after\ntry {\n  const client = await getClient(server);\n} catch (e) {\n  if (e instanceof Error && /is in cooldown/.test(e.message)) {\n    // parse status; do not retry for 4xx, back off for 5xx\n    const m = /HTTP (\\d{3})/.exec(e.message);\n    const status = m ? Number(m[1]) : 0;\n    if (status >= 500) await new Promise(r => setTimeout(r, 5000));\n    else throw e;\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":"function parseCooldownStatus(msg: string): number | undefined {\n  const m = /HTTP (\\d{3}|n\\/a)/.exec(msg);\n  if (!m) return undefined;\n  const n = Number(m[1]);\n  return Number.isFinite(n) ? n : undefined;\n}\n\n// before calling getClient, optionally inspect prior failures:\n// there is no public cooldown introspection API, so wrap and classify.","typeGuard":"function isMcpCooldownError(e: unknown): e is Error {\n  return e instanceof Error && /is in cooldown/.test(e.message);\n}","tryCatchPattern":"try {\n  const client = await getClient(server);\n} catch (e) {\n  if (e instanceof Error && /is in cooldown/.test(e.message)) {\n    const status = /HTTP (\\d{3})/.exec(e.message)?.[1];\n    const code = status ? Number(status) : 0;\n    if (code >= 500) await backoff(5000); // transient\n    else if (code >= 400 && code !== 429) fixConfig(server); // auth/url\n    else await backoff(5000);\n  } else throw e;\n}","preventionTips":["Fix definitive 4xx (401/403/404) in config rather than waiting out the cooldown.","Avoid hot retry loops around getClient; the pool caches successful connections.","Log cooldown statuses to distinguish config issues from transient 5xx."],"tags":["mcp","cooldown","network","http","retry"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}