Skip to main content

IMPORTANT: Rate limits

GraphQL Rate Limits​

We have rate limits in place for the GraphQL API to ensure fair access and protect server stability.

There are four types of limits, each protecting against a different kind of overload:

LimitWhat it protects againstStatus code
Request rateToo many requests per time window429
Query complexitySingle queries that request too much data200 (GraphQL error)
Server-time budgetCumulative server processing time per clientInformational only; no budget-based rejection
ConcurrencyToo many simultaneous queries across all clients (per-client and global)503

Request rate limits​

Current limits​

Maximum of 3600 requests per 15 minutes (averages at 4 requests per second).

Rate limits for the GraphQL API are per API key, regardless of the IP.

Rate limit headers​

Responses processed by the request-rate limiter include RateLimit headers:

  • RateLimit-Limit — number of requests allowed in the window

  • RateLimit-Policy — informative quota showing the limit with a time window in seconds

  • RateLimit-Remaining — how many requests are remaining in the current window

  • RateLimit-Reset — how many seconds until the rate limit resets

See the IETF draft for rate limit headers. At the moment, we use draft version 06, but we may upgrade to version 08 or newer.

How to handle rate limits​

Avoiding rate limits​

When you get a response from the API, check for the following.

  • Is the HTTP status 200? If not, and if it’s 429 (Too Many Requests), continue to the When you get rate limited section.

  • Does the Ratelimit-Remaining header have a sufficient value for your needs? If not, you may need to change your processes.

When you get rate limited​

Do not attempt to request more data until the time in RateLimit-Reset header passes (e.g. time of your request + reset seconds). We recommend waiting an extra second afterwards so you don’t hit a rate limit right when the rate limit period is ending.

Add a timeout to your request until you can request more data.

If you keep hitting rate limits repeatedly, we will reach out to you.

Query complexity limits​

Every GraphQL query has a complexity score based on how much data it requests. Queries that exceed the complexity limit are rejected before execution.

The main cost driver is nested connections with large page sizes. The first (or last) argument on connections determines how many items are fetched, and nested connections multiply.

How complexity is calculated​

  • Each field costs 1 point

  • Connection fields (like steamApps, followerHistory) cost proportional to their first/last argument

  • Nested connections multiply. steamApps(first: 100) with followerHistory(first: 100) inside costs roughly 100 x 100 = 10,000 effective rows

Examples​

Query patternApprox. rowsComplexityResult
steamApps(first: 50) — flat, several fields50~350Allowed
steamApps(first: 1000) — flat1,000~7,000Allowed
steamApps(first: 10) { followerHistory(first: 90) }900~3,600Allowed
steamApps(first: 100) { followerHistory(first: 365) }36,500~146,000Allowed
steamApps(first: 200) { followerHistory(first: 200) }40,000~161,000Allowed
steamApps(first: 400) { followerHistory(first: 400) }160,000~642,000Allowed
steamApps(first: 500) { followerHistory(first: 500) }250,000~1,002,000Rejected
steamApps(first: 1000) { followerHistory(first: 1000) }1,000,000~4,004,000Rejected

Current limits​

LimitValue
Complexity800,000 max
Depth10 levels of nesting
Breadth100 field selections
Page sizeUp to 1,000 items per ordinary connection (first/last); specialized connections can have lower limits, including 100 for demos and DLC

Complexity response data​

Responses that reach complexity analysis include the query's complexity score so you can monitor your usage. Earlier rejections, such as request-rate and concurrency limits, may omit this data.

Response extensions (in the JSON body):

{
"data": { ... },
"extensions": {
"queryComplexity": {
"score": 31411,
"limit": 800000
}
}
}

Response header: X-Query-Complexity: 31411

How to reduce complexity​

  • Reduce first/last on your connections — this is the biggest lever

  • Paginate using after cursors instead of requesting large pages

  • Flatten — requesting 1 app with 1,000 history entries is much cheaper than 100 apps with 100 history entries each

  • Separate queries — split large requests into multiple smaller ones (each query's complexity is calculated independently)

When a query is rejected​

You'll receive a GraphQL error with the complexity score and limit:

{
"errors": [{
"message": "Query exceeds maximum complexity (complexity: 1002001, max: 800000)"
}],
"extensions": {
"queryComplexity": {
"score": 1002001,
"limit": 800000
}
}
}

Reduce the first/last values on your connections, especially nested ones.

Concurrency limit​

The API limits the number of queries that can execute simultaneously, both per client and globally. This protects the database from being overwhelmed when many heavy queries arrive at the same time, and ensures one client can't monopolize all available capacity.

There are two layers:

  • Per-client limit — each API key can have at most a few queries executing at the same time. If you exceed this, you'll receive a 503 with a message indicating your queries are the bottleneck.

  • Global limit — the total number of concurrent queries across all clients is capped. If the server is at capacity due to combined load from multiple clients, you'll receive a 503.

When you receive a 503 Service Unavailable response, retry after a few seconds. These limits are transient — slots free up as queries complete.

Response headers:

  • X-Concurrent-Queries — current number of in-flight queries (global)

  • X-Concurrent-Limit — maximum allowed concurrent queries (global)

  • X-Concurrent-Queries-Self — current number of your in-flight queries

  • X-Concurrent-Limit-Self — maximum allowed concurrent queries per client

Server-time budget​

The server-time budget tracks how much cumulative server processing time your API key consumes within a fixed 15-minute window. This highlights usage patterns where individual queries are within complexity limits but collectively consume too many server resources.

Server-time budget is informational only until further notice.

How it works​

Each API key has a budget of server processing time per window. Windows align to wall-clock quarter-hours, rather than rolling forward with each request. Query processing time counts toward this budget. Usage is tracked and reported, but exceeding the budget does not currently reject requests. Request-rate, complexity, and concurrency limits still apply independently.

Response data​

Responses that reach the budget middleware include usage headers when tracking is available. GraphQL results include the budget extension when complexity and budget data are available. Responses rejected earlier, or requests where tracking is unavailable, may omit this data.

Response extensions (in the JSON body):

{
"data": { ... },
"extensions": {
"serverTimeBudget": {
"budgetSeconds": 300,
"usedSeconds": 42.5,
"resetSeconds": 279
}
}
}

Response headers:

  • X-Server-Time-Budget — total budget in seconds

  • X-Server-Time-Used — server time used in current window (seconds)

  • X-Server-Time-Reset — seconds until the current window resets

How to stay within budget​

  • Use smaller page sizes — steamApps(first: 100) is much faster than steamApps(first: 1000)

  • Reduce nested connection sizes — ccuHistory(first: 90) instead of ccuHistory(first: 360)

  • Space out requests — usage resets at the next fixed 15-minute window

  • Monitor your usage — check the serverTimeBudget extension when present

When you exceed the budget​

Budget enforcement is currently disabled: exceeding the server-time budget alone does not produce a 429 response or require a retry. Use the usage data to reduce expensive queries and spread requests out. If you receive a request-rate 429, follow the separate request rate limit instructions.