Skip to content

tower: overview cache burns the user's entire GitHub GraphQL quota (3 gh calls × N workspaces every 30s, failures uncached) — every gh command on the machine then fails #1645

Description

@waleedkadous

Symptom (2026-09-07/08, this machine)

Every gh GraphQL call on the account — gh pr view/list/checks, gh issue view, consult PR search, afx spawn's issue fetch — fails with API rate limit already exceeded for user ID … for most of every hour, while gh api rate_limit misleadingly reports 5000/5000 (the response headers show X-RateLimit-Resource: graphql, Used: 5000, Remaining: 0). Builders cannot run CMAP PR reviews, the architect cannot merge via gh pr merge, and the Tower dashboard/cloud UI "times out" because overview requests wait on hanging gh spawns.

Cause, measured

OverviewCache (servers/overview.ts, TTL 30 s) backs each workspace's overview with three GraphQL-backed CLI calls — gh issue list --limit 200 --json …, gh issue list --state closed --search closed:>…, gh pr list --state merged --search merged:>… — via executeForgeCommand. With 13 active workspaces polled (cloud UI + dashboard + VS Code extension every 60 s) the steady state is 13 × 3 × (3600/30) ≈ 4,700 GraphQL calls/hour: the whole 5,000-point budget, forever.

Two amplifiers once the limit is hit:

  1. Failures are not cached. executeForgeCommand returns null on a non-zero exit (lib/forge.ts:336/352) and the cache only stores non-null (if (data !== null) cache.set(...)), so every overview request re-spawns all three commands immediately. Observed: 51 gh spawns from the Tower daemon in 40 s (≈4,600/h) — faster than the TTL allows.
  2. No rate-limit awareness. Nothing parses the rate-limit error or the reset header; Tower keeps hammering until the hour rolls over, and the first requests after reset re-exhaust it.

Cost per call is also higher than 1 point: --limit 200 with author/assignees/labels expansions is a multi-hundred-node GraphQL query.

Fix (BUGFIX, prescribed)

  1. Negative cache + backoff in OverviewCache: a failed forge fetch stores a null result with its own TTL (start 60 s, double to 15 min) so a failing workspace costs one spawn per window, not one per request.
  2. Rate-limit awareness: detect the GraphQL rate-limit error (stderr rate limit already exceeded or graphql_rate_limit), read X-RateLimit-Reset via a single gh api rate_limit --jq .resources.graphql, and suspend ALL GitHub-backed overview fetches until that instant. Surface it in the overview payload (forgeStatus: 'rate-limited', resetAt) so the dashboard shows why data is stale instead of timing out.
  3. Raise the positive TTL to 120 s and refresh only workspaces with a live SSE client or an overview request in the last TTL window; a workspace nobody is looking at must cost zero calls.
  4. Collapse the three calls into one GraphQL query (gh api graphql) per workspace, or move the two --search calls to REST (gh api search/issues, separate 5,000 budget). One request per workspace per window, budgeted at ≤ 1/13 of the hourly quota for 13 workspaces.
  5. Doctor check: codev doctor reports the GraphQL budget (used/limit, reset time) and the projected hourly spend N_workspaces × calls × 3600/TTL, warning when the projection exceeds 50 % of the limit.
  6. Acceptance: with 13 workspaces open and the dashboard polling, the Tower daemon spawns ≤ 60 gh processes per hour at steady state (count children of the Tower pid over 10 min); a forced rate-limit error produces zero further spawns until reset; full suite green.

Refs: Spec 0126 (overview cache), #1641 (consult also blocked by the exhausted quota), #1629 (same week's Tower incident).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions