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

  1. Inspect the embedded status and message; for 401/403/404 fix the server config (URL, headers, credentials) rather than retrying.
  2. For 5xx transient failures, wait out the 5s cooldown (or call evictFromPool(server) once you have reason to believe the upstream recovered).
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/617bc4dd25526112. Report an issue: GitHub.