Skip to content

Review global-401/429 guard thresholds once real log data exists #7

Open

Description

@longphamdev

Context

The global-outage guards (added in #5) use fixed thresholds tuned for the
minimum pool size, with no production data behind them:

  • global401WindowMs = 30s, trigger = ≥ 2 DISTINCT accounts 401ing
  • same window for 429, trigger = ≥ 2 distinct accounts
  • escape hatch: suppression lifts after more than 1 rejection per account
    within the window

Why this needs real data

With exactly 2 accounts, two genuine per-account failures in 30s look
identical to an upstream-wide incident. The suppression heuristic
(repeated bursts → allow marking) mitigates the stuck-forever case, but
the threshold itself is a guess.

What to review (after ~1 week of production logs)

  1. Unknown-error log lines (status + redacted headers + body preview)
    accumulated since deploy — do real 429s carry distinguishing
    headers/biz codes that beat the timing heuristic?
  2. Did any global-401/429 suspicion fire? If yes: was it actually global
    (both accounts were fine later) or per-account (both were dead)?
  3. Is 30s the right window for Claude Code traffic patterns (bursts of
    subagent requests)?

Inputs available

docs/multi-account.md documents the current thresholds.

Activity

  1. longphamdev commented on Nov 9, 2026

    @longphamdev
    OwnerAuthor

    Dữ liệu production đầu tiên (E2E 2 acc thật, 2026-11-09):

    Setup: acc1 = start-plan hết quota (biz 1005), acc2 = start-plan mới login, cả hai cùng IP.

    Kết quả:

    1. biz-1005 classification hoạt động production: pool: account <id> quota exhausted (biz 1005) — matcher theo body OK, không miss.

    2. Failover thật đầu tiên: request → acc1 (1005) → exhausted → rotate → acc2 → 200 OK. Body replayable, client không thấy lỗi. Sau đó 5 request kế đi thẳng acc2 (exhausted được nhớ) — không đốt 1005 lặp lại. Đúng thiết kế.

    3. 429 thật captured (điều ta chưa từng có mẫu): 6 concurrent requests → 3× 200, 1× upstream 429 trên acc2, 2× 503 (all-busy khi hit cap + acc1 exhausted). acc2 bị rate limit với chỉ 3 stream đồng thời — acc free rate limit rất thấp.

    4. Quan sát cần review (lý do issue này tồn tại):

      • acc2 cooling kéo dài > 2 phút sau một 429 duy nhất — Retry-After từ upstream có vẻ dài, hoặc global-429 guard (2 acc: 1 exhausted + 1 cooling) đã coolAll trong lúc acc1 đã exhausted sẵn → hiệu ứng "cả pool khóa" dễ xảy ra khi pool = 2 và 1 đã dead.
      • Half-open probe hoạt động: acc1 được probe sau ~30 phút → 1005 → exhausted lại. Chu kỳ đúng.
    5. Đề xuất xem lại:

      • Global-429 guard nên EXCLUDE các acc đã exhausted/expired khỏi phép tính "distinct accounts" — chỉ đếm 429 trên acc healthy.
      • Log Retry-After giá trị thật vào cooling line để đo đạc.

    Verify khẳng định hoạt động: no leaked slots (mọi request xong inFlight về 0), không watchdog warning, stream dài chưa test (cần dùng thật thêm).

  2. longphamdev commented on Nov 9, 2026

    @longphamdev
    OwnerAuthor

    Follow-up on the live findings (fixes deployed):

    1. Poll-0 vs successful requests — implemented the ground-truth rule: a
      successful REQUEST invalidates a stale remainingUnits: 0 poll reading
      (pool clears it on noteSuccess). The poller itself never marks exhausted
      from remaining=0 (verified — it only feeds data + heals). /quota now
      flags a healthy account reading 0 as remainingUnreliable: true instead
      of presenting 0 as fact.

    2. Global-429 guard fix — usableCount() (healthy/cooling) now drives
      single-acc parity AND the global guard; exhausted/expired accounts are
      excluded. With a 2-acc pool where one is dead, the live account behaves
      as single-acc (pass-through parity) instead of coolAll-ing a pool with
      nowhere to rotate. Terminal precedence: cool() is a no-op on
      exhausted/expired — a stray 429 on a half-open probe cannot resurrect the
      account early.

    3. Internal cooling clamp — pool cooling is capped at 6h even when the
      upstream Retry-After is huge (weird header must not lock an account out
      for days). The client still sees the verbatim header.

    4. maxInFlightPerAccount lowered 4 → 2 — free-tier 429'd with ~3
      concurrent streams; the 5s queue now absorbs bursts instead of feeding
      the upstream limit. Will watch AccountsBusyError rate.

    Measured so far: upstream 429 hit acc2 with 3 concurrent streams
    (Retry-After value not yet captured — the passthrough path was dropping the
    header; now forwarded verbatim on both response paths, so next occurrence
    will be measurable).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions