Rate limits
A per-key token bucket governs request rates; a hard monthly quota governs volume.
Two independent caps apply to every request:
- A rate limit — a token bucket smooths short-term throughput: 20 requests per second sustained, with a 100-request burst. Applied per API key.
- A monthly quota — the standard plan includes 20,000 requests per calendar month, enforced as a hard cap: requests past it are rejected until the month resets. Applied per account, shared across all of that account's keys — creating a second key does not grant a second allowance.
They bind independently: you can be well inside your monthly quota and still get a 429 for
bursting, and vice versa.
How the bucket behaves. The bucket holds 100 tokens and refills at 20 per second. Every request
takes one. A burst of up to 100 back-to-back requests goes straight through from a full bucket;
sustained traffic above 20/sec drains it and starts returning 429 until it refills. A short pause
restores capacity — five seconds of silence refills the bucket completely.
In practice the monthly quota binds long before the rate limit does: 20 requests per second is 1.7 million a day, and your monthly allowance is 20,000. Treat the rate limit as a guard against runaway loops, and the quota as the number to actually design around — see how often to poll.
All keys see the same endpoints and data — the plan changes throughput and included volume, not features.
During a free trial
While your subscription is in its trial period there are two limits, counted from the start of the trial rather than per calendar month:
| Limit | Value |
|---|---|
| Total trial requests | 5,000 |
| Any single endpoint | 1,000 |
The per-endpoint ceiling is what lets the total be generous. It is sized so a normal evaluation never notices it — spreading a few thousand calls across matches, stats, odds, teams and players stays comfortably inside it — while a script pointed at one endpoint to bulk-load history stops at 1,000 rather than draining the whole allowance.
A trial request past the total returns 429:
// 429 Too Many Requests
{
"error": {
"code": "trial_quota_exceeded",
"message": "trial request allowance reached — subscribe at https://esportsodds.gg/app/settings/plan to continue",
"request_id": "01a0..."
}
}A request past the per-endpoint ceiling returns 429 with a different code, because your
overall allowance still has requests left — switch endpoints and you can keep working:
// 429 Too Many Requests
{
"error": {
"code": "trial_endpoint_quota_exceeded",
"message": "this endpoint's trial allowance is used up (1000 of 1000 on /v1/{game}/odds). Your overall trial allowance still has requests left — try another endpoint, or subscribe at https://esportsodds.gg/app/settings/plan to lift both limits.",
"request_id": "01a0..."
}
}Subscribing from that page ends the trial immediately, starts the paid period, lifts you to the full 20,000, and removes the per-endpoint ceiling entirely.
What counts
Every authenticated REST request draws one token and one unit of monthly quota. When your bucket is empty, further requests are rejected until it refills:
// 429 Too Many Requests
{
"error": {
"code": "rate_limited",
"message": "rate limit exceeded",
"request_id": "01a0..."
}
}Back off and retry after a short delay. Spikes are smoothed by the bucket's burst capacity; steady
throughput above your refill rate is what triggers 429s.
When your monthly quota is used up, requests are rejected until the next calendar month starts:
// 429 Too Many Requests
{
"error": {
"code": "quota_exceeded",
"message": "monthly quota exceeded",
"request_id": "01a0..."
}
}How often to poll
Your whole monthly allowance is 20,000 requests across roughly 720 hours — about 28 requests per hour, for everything you do. Budget against that number, not against the per-second rate limit, which you will almost never reach.
The single most useful thing to know is that the fixture list and the odds move at completely different speeds. Measured over the last 7 days of production data:
| What you'd poll | How often it actually changes |
|---|---|
/v1/cs2/matches — fixtures, scores, status | ~5.9 writes/hour |
/v1/cs2/odds — the market line | ~665 writes/hour |
That's a 113× difference, so polling them on the same timer wastes most of your allowance on an endpoint that hasn't changed. Two 30-day examples:
| Strategy | Requests/month | Share of plan |
|---|---|---|
| Both endpoints every 5 minutes | 17,280 | 86% |
| Odds every 5 minutes, fixtures hourly | 9,360 | 47% |
The second is the same odds freshness for a bit over half the cost. Note what the first row means honestly: a 5-minute poll on both endpoints consumes most of the plan on its own, before any per-match detail calls. If that's your access pattern, the numbers above are the ones to plan around.
Some ways to spend less:
- Open a WebSocket instead of polling for changes. It costs no request quota at all — not the ticket, not the connection, not the messages — and it pushes score and odds changes for as long as it stays open. For anything change-driven this is not merely cheaper than a timer, it is free. See Live data & WebSocket.
- Poll fixtures on a slow timer — hourly is ample at ~6 writes/hour, and a daily schedule refresh is fine for most uses.
- Filter server-side.
?status=live,?date_from=/?date_to=and?tournament=cost the same one request as an unfiltered call but save you paging through rows you'll discard. - Ask for detail only where it exists. Each match row carries
data_available, and?has=rounds,depthfilters the list to matches that actually have those sub-resources — so you never spend a request discovering there was nothing to fetch.
Response headers
Every metered response tells you where you stand — no need to count client-side:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your token bucket's capacity — the burst allowance (100 on the standard plan). |
X-RateLimit-Remaining | Whole tokens left in the bucket after this request. |
X-Quota-Limit | Your request quota (20,000/month on the standard plan; 1,000 during a trial). |
X-Quota-Remaining | Requests left in the current window, across every key on the account. |
X-Quota-Reset | When the window ends (RFC 3339, UTC — the start of next month; during a trial, the trial's end). |
Either kind of 429 also carries Retry-After — the number of seconds to wait before retrying
(a second or two for a rate-limit 429; up to the end of the window for a quota 429).
Calling from a browser? These are custom headers, so a cross-origin fetch can only read them
because the API names them in Access-Control-Expose-Headers. It does — all six above, plus
Retry-After. See CORS and browser clients.
Checking your usage
The headers above are authoritative per request; your dashboard shows the same request volume and remaining allowance for each key. Rate-limit and quota enforcement live in the API itself; the dashboard reads the same usage events the API records per request.
WebSocket connections
The WebSocket channel is metered differently from REST — a persistent connection isn't a stream of discrete requests, so the token-bucket model doesn't map cleanly onto it. It's a tier-gated feature, capped by concurrent connections rather than metered per message:
- Concurrent-connection cap. On the standard plan a key may hold 5 concurrent connections
(the cap is tier-based). Opening one past the cap is refused with an
errorframe (code: "connection_limit") followed by a close — not a queue. - Nothing about the WebSocket consumes request quota. Neither
POST /v1/{game}/ws-tokennor opening the connection draws on your monthly or trial allowance, and the messages pushed over an open connection are not billed either — an open subscription accrues no per-message or per-ping charges for its lifetime. Tickets are short-lived (60 seconds), so a client that reconnects mints a new one each time; that reconnection is free. The concurrent-connection cap above is the only limit that applies. - The ticket mint is still rate-limited. It draws a token from the per-second bucket like any
other call, so a tight reconnect loop will still see
429s — reconnect with backoff. - Disconnecting frees the slot immediately, including a keep-alive timeout close.
Reuse one connection across many matches (that's what
subscribeis for) rather than opening one per match, and reconnect with backoff rather than in a tight loop.