janhq/jan · error · Error

API key rotation exhausted

Error message

API key rotation exhausted

What it means

Thrown by createApiKeyRotatingFetch after the fetch wrapper cycled through every entry in the apiKeys rotation chain. The loop only continues on 401/403/429 (auth/rate-limit) responses while another key remains; reaching this throw means each key was attempted and each was rejected with one of those statuses. It is the terminal guard at the bottom of the rotation for-loop (model-factory.ts:778).

Source

Thrown at web-app/src/lib/model-factory.ts:778

  ): Promise<Response> => {
    for (let i = 0; i < apiKeys.length; i++) {
      const key = apiKeys[i]!
      const nextHeaders = new Headers(init?.headers as HeadersInit | undefined)
      if (headerMode === 'authorization-bearer') {
        nextHeaders.set('Authorization', `Bearer ${key}`)
      } else if (headerMode === 'x-goog-api-key') {
        nextHeaders.set('x-goog-api-key', key)
      } else {
        nextHeaders.set('x-api-key', key)
      }
      const res = await inner(input, { ...init, headers: nextHeaders })
      if ([401, 403, 429].includes(res.status) && i < apiKeys.length - 1) {
        res.body?.cancel().catch(() => {})
        continue
      }
      return res
    }
    throw new Error('API key rotation exhausted')
  }
}

// An empty apiKey still puts an empty auth header on the wire, which upstreams
// answer with misleading 401s (e.g. Anthropic's "x-api-key header is
// required"). Fail here with an actionable message instead.
function requireRemoteApiKey(
  provider: ProviderObject,
  keyChain: string[]
): string {
  const key = keyChain[0] ?? provider.api_key?.trim()
  if (!key) {
    throw new Error(
      `No API key configured for ${provider.provider}. Add one in Settings > Model Providers.`
    )
  }
  return key
}

View on GitHub (pinned to fad3f12a14)

Solutions

  1. Open Settings > Model Providers and replace at least one API key with a freshly issued, verified key.
  2. If 429s dominate, reduce concurrency / add backoff before retrying — rotation is not a substitute for rate-limit headroom.
  3. Confirm the keys are valid with a direct curl to the provider's /models or /chat/completions endpoint.
  4. Check provider.api_key and the keyChain source (keyring) are not all empty strings, which rotate but never authenticate.

Example fix

// before: rotate silently, surface only 'exhausted'
// after: surface the last response body so the user sees the provider's reason
const res = await inner(input, { ...init, headers: nextHeaders })
if ([401, 403, 429].includes(res.status) && i < apiKeys.length - 1) {
  res.body?.cancel().catch(() => {})
  continue
}
return res
// (loop fallthrough now impossible; keep the throw as a defensive invariant)
Defensive patterns

Strategy: validation

Validate before calling

function pickFirstValidKey(apiKeys: string[]): string | undefined {
  return apiKeys.find(k => typeof k === 'string' && k.trim().length > 0)
}
// before constructing createApiKeyRotatingFetch:
if (!pickFirstValidKey(apiKeys)) {
  throw new Error('No valid API key available for rotation')
}

Type guard

function hasUsableKeyChain(keys: unknown): keys is string[] {
  return Array.isArray(keys) && keys.some(k => typeof k === 'string' && k.trim().length > 0)
}

Try / catch

try {
  return await createApiKeyRotatingFetch(fetch, apiKeys, params, headerMode)(url, init)
} catch (e) {
  if (e instanceof Error && e.message === 'API key rotation exhausted') {
    // surface a settings deep-link; do not retry with the same keys
    throw new Error('All API keys were rejected. Update them in Settings > Model Providers.')
  }
  throw e
}

Prevention

When it happens

Trigger: A provider is configured with multiple API keys (keyChain length > 1) and every key returns 401 (invalid), 403 (forbidden), or 429 (rate-limited) on the chat-completion request. The inner fetch runs once per key, cancels the body on those statuses, and continues until i exhausts apiKeys.length.

Common situations: All rotated keys were revoked or expired at once; the user pasted the same dead key into every slot; the provider hit a platform-wide rate limit so even healthy keys 429; a corporate proxy strips auth headers so every key looks invalid.

Related errors


AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12). Data as JSON: /api/errors/fcbe7637d042aa4e. Report an issue: GitHub.