Skip to content

Fail over to next account on 429 instead of blocking - #13

Closed
brendandebeasi wants to merge 1 commit into
KarpelesLab:masterfrom
brendandebeasi:fix/429-failover
Closed

brendandebeasi wants to merge 1 commit into
KarpelesLab:masterfrom
brendandebeasi:fix/429-failover

Conversation

@brendandebeasi

Copy link
Copy Markdown

Summary

When an account returns a 429 (quota exhaustion or rate limit), the proxy treated it as transient: it slept for the full retry-after and retried the same account, never marking it unavailable. For genuine quota exhaustion retry-after can be hours, so the client connection hangs until the client (e.g. Claude Code) gives up with an "API overloaded" error. The only workaround was restarting the proxy, which re-probes every account and routes around the dead one.

This changes the 429 path to:

  • Mark the account rate-limited for the retry-after window (markRateLimited), so getActiveAccount() skips it.
  • Immediately fail over to the next available account (no blocking sleep).
  • When all accounts are throttled, return a 429 to the client with a correct retry-after to back off on, instead of holding the connection open.

Test plan

  • Single account hits 429 → client receives 429 with retry-after immediately (no long hang).
  • One of N accounts hits 429 → request transparently fails over to another account and succeeds.
  • All accounts throttled → client gets 429 with the soonest reset as retry-after.

A 429 (quota exhaustion or rate limit) was treated as transient: the
proxy slept for the full retry-after and retried the same account,
never marking it unavailable. For real quota exhaustion retry-after can
be hours, so the client connection hung until Claude Code gave up with
an "API overloaded" error; only a restart (which re-probes every
account) routed around the dead account.

Now a 429 marks the account rate-limited for the retry-after window and
immediately fails over to the next available account. When every
account is throttled the client gets a 429 with a correct retry-after
to back off on, instead of a hung connection.
daniel-rudaev added a commit to D1DX/teamclaude that referenced this pull request May 30, 2026
- 429: mark account rate-limited and fail over to the next available
  account instead of sleeping retry-after on a dead account (PR KarpelesLab#13).
- 529: hold the client and retry with exponential backoff (cap 60s/wait)
  for ~1h, then return 529.
- OAuth-beta: keep oauth-2025-04-20 in anthropic-beta on OAuth requests
  to avoid spurious 401s (PR KarpelesLab#3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MagicalTux added a commit that referenced this pull request Jun 18, 2026
One Anthropic account (email/accountUuid) can hold multiple subscriptions
across different organizations — e.g. a corporate Pro org and a personal Max
org. The old identity model keyed only on accountUuid (with name as fallback),
so adding the second org overwrote the first (#6) and --name couldn't help
because the UUID check won (#7).

Identity is now (accountUuid + organization). Re-implemented and reviewed
against master rather than cherry-picking the closed fork PRs (#15/#17).

- src/identity.js (new): orgKey / sameIdentity / matchAccounts / emailOf, the
  single source of truth for identity and lookups. sameIdentity treats an
  unknown org on either side as a match so a freshly-profiled login backfills a
  legacy entry instead of duplicating it.
- fetchProfile: extract organization.uuid; accounts persist orgUuid/orgName.
- All dedup/sync/find match sites switched to sameIdentity; syncAccountsFromDisk
  uses a greedy 1:1 claim so multiple same-person/different-org entries pair
  correctly. accounts command dedups by (accountUuid, org) and backfills org.
- Name disambiguation: one org -> "email"; multiple -> "email (Org)", renaming
  the existing collider too. Names stay unique (they are the user-facing key).
- Org-aware remove + api: resolve by name or email, --org <name|uuid> to
  disambiguate; lists candidates when ambiguous.
- Per-account rotation priority: `priority` field (lower = preferred, default 0)
  as the primary sort key in _selectNext with the existing weekly heuristic as
  tiebreaker; new `teamclaude priority` command (--first/--last); session-reset
  switching won't demote to a worse priority.
- Tests: identity/matchAccounts/emailOf and priority rotation (22 passing).
- Docs: README, config.example.json, help text.

Out of scope (future PRs from the #18 stack): #14 headless/reload API/prober,
#16 usage-endpoint quota. #13 (429 account-failover) intentionally not taken —
the throttle is IP-based, so the merged #25 back-off is the correct behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant