Rate limits

A per-key token bucket governs request rates; a hard monthly quota governs volume.

Two independent caps apply to every request:

  1. A rate limit — a token bucket smooths short-term throughput: 20 requests per second sustained, with a 100-request burst. Applied per API key.
  2. 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:

LimitValue
Total trial requests5,000
Any single endpoint1,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 pollHow 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:

StrategyRequests/monthShare of plan
Both endpoints every 5 minutes17,28086%
Odds every 5 minutes, fixtures hourly9,36047%

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,depth filters 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:

HeaderMeaning
X-RateLimit-LimitYour token bucket's capacity — the burst allowance (100 on the standard plan).
X-RateLimit-RemainingWhole tokens left in the bucket after this request.
X-Quota-LimitYour request quota (20,000/month on the standard plan; 1,000 during a trial).
X-Quota-RemainingRequests left in the current window, across every key on the account.
X-Quota-ResetWhen 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 error frame (code: "connection_limit") followed by a close — not a queue.
  • Nothing about the WebSocket consumes request quota. Neither POST /v1/{game}/ws-token nor 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 subscribe is for) rather than opening one per match, and reconnect with backoff rather than in a tight loop.

On this page