Rate limits & quotas
Two independent limits apply to every key:
- Request rate — requests per minute, per key. Exceeding it returns a
429with problem typerate-limited. - Monthly row quota — rows downloaded per calendar month, per account.
Exhausting it returns a
429with problem typequota-exhausted, plusreset(when it clears) andupgrade_urlextensions.
A third, rare 429 (problem type throttled) is an operations kill-switch
on a specific key — see the three 429s.
Per-tier request rates
Section titled “Per-tier request rates”| Tier | Requests per minute |
|---|---|
| DEMO | 30 |
| BETA | 120 |
| STARTER | 300 |
| PRO | 1000 |
Monthly row quotas for the paid tiers are still being finalized; while your
tier has no cap configured, GET /v1/me reports monthly_rows: null and no
quota gate applies.
Quota = rows downloaded
Section titled “Quota = rows downloaded”Every dataset request debits the artifact’s full row count — the number of rows in the Parquet file, not the number you end up using:
- Filters don’t reduce spend. The server never filters; the SDK/CLI
filter locally after download.
--operator FANDUELshapes what you see, not what you’re charged. - Range reads debit in full. A partial (HTTP
Range) read — including DuckDB’s predicate-pushdown reads — is still a read of that artifact. - Discovery is cheap. JSON endpoints debit the rows they return (a page of games is a page of rows).
The lever you control is which files you fetch: narrow date ranges and per-game fetches are the cheap path.
Typical rows per game: closing-lines ≈ 2,400 · market-results ≈ 12,000 ·
sportsbook-lines ≈ 190,000 · game-state-timeline is small.
Checking your limits and usage
Section titled “Checking your limits and usage”GET /v1/me reports the calling key’s tier, rate limit, and quota usage. It
stays reachable even when your monthly quota is exhausted, and reading it
never consumes rows:
curl -s "https://api.betflux.ai/v1/me" \ -H "Authorization: Bearer $BETFLUX_API_KEY"{ "key_id": "…", "tier": "PRO", "rate_limit_per_minute": 1000, "quota": { "monthly_rows": null, "used_rows": 412300, "resets": "2026-08-01" }}monthly_rows: null means no row cap is configured for the tier;
used_rows is a best-effort counter of rows downloaded this month; resets
is the UTC date the monthly quota rolls over. betflux keys check prints the
same information.
Headers on 429
Section titled “Headers on 429”Successful responses carry no rate-limit headers — use /v1/me to see
your limits and usage. When a request is rejected with a rate-limit 429,
the response carries the
IETF RateLimit headers:
| Header | Meaning |
|---|---|
RateLimit-Limit |
Requests allowed per minute for your tier |
RateLimit-Remaining |
0 — the window is spent |
RateLimit-Reset |
Seconds until the window resets |
Retry-After |
Seconds to wait before retrying |
A quota-exhausted 429 also carries Retry-After, but it counts down to
the monthly reset — don’t sleep on it. Stop until the reset date in
the body, or upgrade via its upgrade_url.
Practical guidance
Section titled “Practical guidance”- Fetch each game’s file once and query it locally as many times as you like — re-downloading is what costs quota.
- Use
/v1/gamesfilters (league, date range, team, status) to shrink the set of files you fetch; the SDK forwards itsteamfilter to discovery for exactly this reason. - The Python SDK retries rate-limit 429s and 5xx automatically with backoff
that honors
Retry-After(capped at 120 s); quota 429s raiseQuotaExceededErrorimmediately. --limit/max_rowson SDK range queries stops fetching further games once satisfied — it caps spend, not just output.