go2llm API documentation

429 rate_limit_error: what actually triggers it and how to retry properly

A 429 says the request was valid and the key was good — you sent it too soon. Two different conditions share the status code, and they need opposite responses, so the first thing to do is read error.code, not the status.

Written 2026-09-12.

Two errors under one status

rate_limit — too many requests per unit of time. On go2llm the default is 20 requests per minute per key. Waiting fixes it. This is the one you retry.

quota — the key has spent its dollar allowance. Waiting does not fix it; only raising the quota or issuing another key does. Retrying this in a loop will spin until your process is killed.

{
  "error": {
    "type": "rate_limit",
    "code": "rate_limit",
    "message": "rate limit exceeded",
    "request_id": "gw_8b3d2f1a9c7e4056b8d1a2c3e4f50617"
  }
}

go2llm sends a Retry-After header in seconds alongside a 429. Anthropic returns rate_limit_error with a similar header; OpenAI returns rate_limit_exceeded. All three agree on the header, and it is the only number in the exchange that reflects the server's actual state — prefer it over anything you compute.

Note that Anthropic also has a distinct overloaded_error with status 529, which is not a rate limit at all: it means the provider itself is saturated. Treat it like a 503.

What actually triggers it

Three patterns account for most 429s in practice.

Parallel workers with no shared limiter. Ten threads each politely waiting a second between calls still produce ten requests per second in aggregate. The limit is per key, not per thread. Either share one limiter across the pool, or issue a key per worker.

A retry storm. Ten workers hit one 429, all sleep exactly 2 seconds, all wake at the same instant and hit it again. Backoff without jitter converts one collision into a self-sustaining loop. This is the single most common way a 429 turns from a hiccup into an outage.

A coding agent in a tight loop. Agents make many small calls in bursts — tool call, result, next tool call — and a burst of twenty-five in a minute crosses a 20 RPM ceiling easily while the average rate looks fine.

A retry loop that works

Three rules: retry only the retryable codes, honour Retry-After when it comes, and always add jitter.

import random, time
import httpx

def call_with_retry(payload, api_key, max_attempts=5, base=1.0, cap=32.0):
    with httpx.Client(timeout=120.0) as client:
        for attempt in range(max_attempts):
            r = client.post(
                "https://go2llm.tech/v1/chat/completions",
                headers={"Authorization": f"Bearer {api_key}"},
                json=payload,
            )
            if r.status_code < 400:
                return r.json()

            err = r.json().get("error", {})
            code = err.get("code")

            # A spent quota is not a rate limit: waiting will never clear it.
            if r.status_code == 429 and code == "quota":
                raise SystemExit(
                    f"Key quota exhausted: {err.get('message')}. "
                    f"Raise the quota or use another key. request_id={err.get('request_id')}"
                )

            if r.status_code not in {429, 502, 503} or attempt == max_attempts - 1:
                raise RuntimeError(
                    f"{r.status_code} {code}: {err.get('message')} "
                    f"[request_id={err.get('request_id')}]"
                )

            hint = r.headers.get("retry-after")
            delay = float(hint) if hint and hint.isdigit() else min(cap, base * 2 ** attempt)
            time.sleep(delay * (0.5 + random.random() / 2))  # full-range jitter

The jitter line is the important one. delay * (0.5 + random()/2) spreads a synchronized cohort of workers across a window instead of firing them in unison.

Not hitting the limit in the first place

Retrying is damage control; pacing is the fix. A token-bucket limiter shared by the whole worker pool keeps aggregate throughput under the ceiling without any worker needing to know about the others:

import threading, time

class RateLimiter:
    """Shared across threads: the limit is per key, not per worker."""
    def __init__(self, rpm=20):
        self.interval = 60.0 / rpm
        self.lock = threading.Lock()
        self.next_at = 0.0

    def acquire(self):
        with self.lock:
            now = time.monotonic()
            wait = max(0.0, self.next_at - now)
            self.next_at = max(now, self.next_at) + self.interval
        if wait:
            time.sleep(wait)

limiter = RateLimiter(rpm=20)

Two structural moves help more than any amount of tuning. Issue a separate key per workload — a batch job, a bot and a dev laptop on one key share one 20 RPM ceiling and will starve each other; separate keys get separate ceilings and separate dollar quotas. And batch small calls into fewer larger ones: twenty classification requests at 20 RPM take a minute, while one request classifying twenty items takes a second and usually costs less.

Related errors

FAQ

What is the rate limit on go2llm? 20 requests per minute per key by default, set per key. It is a request-rate ceiling, separate from the key's dollar quota.

Should I retry a 429 automatically? Retry rate_limit, yes — with Retry-After and jitter. Do not retry quota: nothing changes while you wait.

Does a 429 cost money? No. The request never reaches a model, and the cost reservation is released.

Why do I get 429s when my average rate is well under the limit? Because limits apply to bursts, not averages. Twenty-five calls in the first ten seconds of a minute exceed a 20 RPM ceiling even though the minute's average is fine. A shared limiter smooths the burst.


Get a key and make your first request in 3 minutes → go2llm.tech. Registration credits $1 to your balance. Per-model prices are on the pricing page.