zed-industries/zed · error · RuntimeError

GraphQL failed after {retries} retries: {last_err}

Error message

GraphQL failed after {retries} retries: {last_err}

What it means

graphql() exhausted all attempts — each one either hit a transient HTTP status (429/502/503/504) and slept, or raised a RequestException — and the final raise reports the retry count and last_err. When every attempt took the transient-status continue branch, last_err is still None and the message ends with 'retries: None', which itself is the tell that the failures were status-based, not exceptions.

Source

Thrown at script/triage_project_sync.py:227

            if r.status_code == 200:
                data = r.json()
                if "errors" in data:
                    log(f"GraphQL errors: {json.dumps(data['errors'])[:400]}", "ERROR")
                    raise RuntimeError("GraphQL returned errors")
                return data["data"]
            if r.status_code in (429, 502, 503, 504):
                wait = 2**attempt * 2
                log(f"GraphQL {r.status_code}; retry in {wait}s", "WARN")
                time.sleep(wait)
                continue
            log(f"GraphQL HTTP {r.status_code}: {r.text[:300]}", "ERROR")
            r.raise_for_status()
        except requests.RequestException as e:
            last_err = e
            wait = 2**attempt * 2
            log(f"GraphQL threw {e}; retry in {wait}s", "WARN")
            time.sleep(wait)
    raise RuntimeError(f"GraphQL failed after {retries} retries: {last_err}")


# ---------------------------------------------------------------------------
# Issue data fetch


@dataclass
class IssueData:
    number: int
    node_id: str
    title: str
    state: str  # "open" / "closed"
    closed_at: datetime | None
    created_at: datetime
    reporter: str
    assignees: list[str]
    labels: list[str]
    issue_type: str | None  # e.g. "Bug", "Crash", "Meta", "Tracking", or None

View on GitHub (pinned to bc538def45)

Solutions

  1. Check the WARN logs immediately above: they show the status or exception for every attempt
  2. On 429s: space out or batch the queries, and read the rateLimit cost field instead of guessing
  3. Increase retries/backoff for real outages, and abort runs fast when errors are non-transient
  4. If attempts were connection errors, verify runner egress/DNS and the API endpoint URL

Example fix

// before
raise RuntimeError(f"GraphQL failed after {retries} retries: {last_err}")

// after
if last_err is None:
    last_err = RuntimeError("all attempts returned transient HTTP statuses (429/502/503/504)")
raise RuntimeError(f"GraphQL failed after {retries} retries: {last_err}")
Defensive patterns

Strategy: retry

Validate before calling

def graphql_budget_available() -> bool:
    data = graphql("query { rateLimit { remaining cost } }")
    return data["rateLimit"]["remaining"] > data["rateLimit"]["cost"]

Type guard

def is_transient_http_status(status: int) -> bool:
    return status in (429, 502, 503, 504)

Try / catch

try:
    data = graphql(query, variables, retries=5)
except RuntimeError as exc:
    if "retries: None" in str(exc):
        raise SystemExit("All attempts hit transient HTTP statuses (429/5xx); back off and rerun later")
    raise

Prevention

When it happens

Trigger: Sustained 429 secondary rate limiting through all three attempts; a GitHub 5xx outage outlasting the backoff (2s, 4s); every attempt raising ConnectionError/Timeout because the runner's network or DNS is broken.

Common situations: Overlapping CI jobs exhausting the token's rate budget; GitHub incidents; sandboxed runners with blocked egress to api.github.com.

Related errors


AI-assisted analysis of zed-industries/zed@bc538def45 (2026-08-16). Data as JSON: /api/errors/e87f1c9cccd85358. Report an issue: GitHub.