ruvnet/ruflo · error

MCP server " " is in cooldown (HTTP , s remaining)

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

Same cooldown mechanism as the 429 case, but the recorded failure was a non-429 definitive HTTP status (401/403/404/500…) recorded by recordFailure. checkCooldown (clientPool.ts:111) throws this summary error for ~5s (DEFAULT_RATE_LIMIT_COOLDOWN_MS) after such a failure, including the original status and message, so rapid retries get a fast, clear signal instead of re-hitting the upstream.

Solutions

  1. Read the embedded original message/status — fix the root cause (url path, auth headers) in MCP_SERVERS config
  2. Wait out the ~5s cooldown; it clears automatically, or call evictFromPool(server) to clear it immediately after fixing config
  3. Verify the endpoint manually: curl -v with the same URL and headers
  4. For 5xx, check upstream health before retrying

Example fix

// before
const client = await getClient(server); // in cooldown after earlier 401

// after
import { evictFromPool, getClient } from "$lib/server/mcp/clientPool";

// after fixing the MCP_SERVERS url/headers:
evictFromPool(server); // drop stale cooldown for the old (broken) config
const client = await getClient(server);
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

function isCooldownError(e: unknown): boolean {
	return e instanceof Error && /is in cooldown \(HTTP/i.test(e.message);
}

Try / catch

try {
	client = await getClient(server);
} catch (e) {
	if (isCooldownError(e)) {
		// wait out the ~5s window or evict after fixing root cause
		await new Promise((r) => setTimeout(r, 5_000));
		return getClient(server);
	}
	throw e;
}

Prevention

When it happens

Trigger: An earlier connect to the MCP server failed definitively (bad auth 401, wrong path 404, upstream 5xx); getClient() is called again for the same url+headers within the 5s cooldown window and gets this error with the remaining seconds.

Common situations: Wrong MCP_SERVERS url (missing /mcp path) or expired token causing 401/404, followed by quick retries from the UI or a startup loop; upstream 502 blip plus an aggressive retry.

Related errors


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

Appendix: 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 fa13ee4ad6)