ruvnet/ruflo · error · Error
MCP server "${server.name}" is in cooldown (HTTP ${cd.status
Error message
MCP server "${server.name}" is in cooldown (HTTP ${cd.status ?? "n/a"}, ${Math.round(remaining / 1000)}s remaining): ${cd.message} What it means
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.
Source
Thrown at ruflo/src/ruvocal/src/lib/server/mcp/clientPool.ts:111
if (Number.isFinite(seconds) && seconds > 0) {
return Math.min(seconds * 1000, MAX_RATE_LIMIT_COOLDOWN_MS);
}
}
return DEFAULT_RATE_LIMIT_COOLDOWN_MS;
}
function checkCooldown(key: string, server: McpServerConfig): void {
const cd = failureCooldown.get(key);
if (!cd) return;
const remaining = cd.until - Date.now();
if (remaining <= 0) {
failureCooldown.delete(key);
return;
}
if (cd.status === 429) {
throw new McpRateLimitedError(server.name, cd.status, remaining, cd.message);
}
throw new Error(
`MCP server "${server.name}" is in cooldown (HTTP ${cd.status ?? "n/a"}, ` +
`${Math.round(remaining / 1000)}s remaining): ${cd.message}`
);
}
function recordFailure(key: string, status: number | undefined, err: unknown): void {
const message = err instanceof Error ? err.message : String(err);
const cooldownMs = status === 429 ? extractRetryAfterMs(err) : DEFAULT_RATE_LIMIT_COOLDOWN_MS;
failureCooldown.set(key, {
until: Date.now() + cooldownMs,
status,
message,
});
}
export async function getClient(server: McpServerConfig, signal?: AbortSignal): Promise<Client> {
const key = keyOf(server);
const existing = pool.get(key);View on GitHub (pinned to 6b01dc5a68)
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.
Example fix
// before
const client = await getClient(server);
// after
try {
const client = await getClient(server);
} catch (e) {
if (e instanceof Error && /is in cooldown/.test(e.message)) {
// parse status; do not retry for 4xx, back off for 5xx
const m = /HTTP (\d{3})/.exec(e.message);
const status = m ? Number(m[1]) : 0;
if (status >= 500) await new Promise(r => setTimeout(r, 5000));
else throw e;
} else throw e;
} Defensive patterns
Strategy: retry
Validate before calling
function parseCooldownStatus(msg: string): number | undefined {
const m = /HTTP (\d{3}|n\/a)/.exec(msg);
if (!m) return undefined;
const n = Number(m[1]);
return Number.isFinite(n) ? n : undefined;
}
// before calling getClient, optionally inspect prior failures:
// there is no public cooldown introspection API, so wrap and classify. Type guard
function isMcpCooldownError(e: unknown): e is Error {
return e instanceof Error && /is in cooldown/.test(e.message);
} Try / catch
try {
const client = await getClient(server);
} catch (e) {
if (e instanceof Error && /is in cooldown/.test(e.message)) {
const status = /HTTP (\d{3})/.exec(e.message)?.[1];
const code = status ? Number(status) : 0;
if (code >= 500) await backoff(5000); // transient
else if (code >= 400 && code !== 429) fixConfig(server); // auth/url
else await backoff(5000);
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MCP server "${server.name}" returned HTTP ${httpStatus}: ${h
- HTTP transport failed: ${String(firstError instanceof Error
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
- Failed to fetch base servers: ${response.statusText}
- Failed to fetch ${url}: ${response.status} ${response.status
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/617bc4dd25526112.
Report an issue: GitHub.